From 4a9eaa2741ae818530bfcf56a33cb67c90e3fa6c Mon Sep 17 00:00:00 2001 From: Daniel Neighman Date: Thu, 28 Nov 2019 12:18:12 -0800 Subject: [PATCH] Initial Commit --- .formatter.exs | 4 ++ .gitignore | 26 +++++++++++ LICENSE | 20 +++++++++ README.md | 67 ++++++++++++++++++++++++++++ lib/c3p0.ex | 2 + lib/c3p0/blank.ex | 52 ++++++++++++++++++++++ lib/c3p0/humanize.ex | 19 ++++++++ lib/c3p0/id.ex | 78 ++++++++++++++++++++++++++++++++ mix.exs | 61 +++++++++++++++++++++++++ mix.lock | 8 ++++ test/c3p0/blank_test.exs | 96 ++++++++++++++++++++++++++++++++++++++++ test/c3p0/id_test.exs | 94 +++++++++++++++++++++++++++++++++++++++ test/c3p0_test.exs | 4 ++ test/support/blank.ex | 3 ++ test/support/id.ex | 18 ++++++++ test/test_helper.exs | 1 + 16 files changed, 553 insertions(+) create mode 100644 .formatter.exs create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 lib/c3p0.ex create mode 100644 lib/c3p0/blank.ex create mode 100644 lib/c3p0/humanize.ex create mode 100644 lib/c3p0/id.ex create mode 100644 mix.exs create mode 100644 mix.lock create mode 100644 test/c3p0/blank_test.exs create mode 100644 test/c3p0/id_test.exs create mode 100644 test/c3p0_test.exs create mode 100644 test/support/blank.ex create mode 100644 test/support/id.ex create mode 100644 test/test_helper.exs diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f40d45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +.elixir_ls/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +c3p0-*.tar + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ccb45d3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2019 Daniel Neighman + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..bef1d66 --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# C3P0 + +C3P0 contains protocol definitions. + +Centralizing these definitions, it is hoped that libraries and applications will make use of them to make data more portable between components of a system. + +# Protocols + +## ID + +This protocol standardizes finding an ID for a peice of data. + +There are two functions in this protocol: + +1. `C3P0.ID.id(data)` +2. `C3P0.ID.guid(data)` + +By default the guid will fall back to the id. + +This will work on all structs and maps which have fields `id` (and optionally `guid`) + +To customize for your struct if there are different fields, derive the `C3P0.ID` protocol. + +```elixir +defmodule MyStruct do + @derive {C3P0.ID, id_field: :local_id, guid_field: :global_id} + defstruct [:local_id, :global_id] +end +``` + +## Humanize + +The `C3P0.Humanize` protocol displays as a string the data provided. + +Falls back to a simple `to_string` + +## Blank + +The `C3P0.Blank` protocol deals with blankness. + +Empty strings, bbinaries, lists, maps and tuples are considered `blank?` along with `nil`. + +The protocol defines 3 functions: + +* `blank?(data)` - true/false +* `present?(data)` - true/false +* `presence(data)` - returns the value if it is not blank. nil if it is blank + + + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `c3p0` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:c3p0, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at [https://hexdocs.pm/c3p0](https://hexdocs.pm/c3p0). + diff --git a/lib/c3p0.ex b/lib/c3p0.ex new file mode 100644 index 0000000..d8191ad --- /dev/null +++ b/lib/c3p0.ex @@ -0,0 +1,2 @@ +defmodule C3P0 do +end diff --git a/lib/c3p0/blank.ex b/lib/c3p0/blank.ex new file mode 100644 index 0000000..20e49b5 --- /dev/null +++ b/lib/c3p0/blank.ex @@ -0,0 +1,52 @@ +defprotocol C3P0.Blank do + @moduledoc """ + Provides a blankness check to see if something is blank/present + + Empty lists, maps, strings, tuples and binaries are considered blank, as is nil. + + Default structs are also considered blank + """ + + @fallback_to_any true + + @doc "true if the data is blank" + @spec blank?(term) :: boolean + def blank?(data) + + @doc "true if the data is not blank" + @spec present?(term) :: boolean + def present?(data) + + @doc "Returns the value if present or nil" + @spec presence(term) :: term | nil + def presence(data) +end + +defimpl C3P0.Blank, for: Any do + @blank ["", nil, [], %{}, <<>>, {}] + + def blank?(%mod{} = data), do: mod.__struct__() == data + def blank?(data), do: data in @blank + + def present?(data), do: not C3P0.Blank.blank?(data) + + def presence(data) do + if C3P0.Blank.blank?(data) do + nil + else + data + end + end +end + +defimpl C3P0.Blank, for: MapSet do + def blank?(ms), do: MapSet.size(ms) == 0 + def present?(ms), do: not C3P0.Blank.blank?(ms) + def presence(ms) do + if MapSet.size(ms) == 0 do + nil + else + ms + end + end +end diff --git a/lib/c3p0/humanize.ex b/lib/c3p0/humanize.ex new file mode 100644 index 0000000..9b3ad63 --- /dev/null +++ b/lib/c3p0/humanize.ex @@ -0,0 +1,19 @@ +defprotocol C3P0.Humanize do + @moduledoc """ + Converts a data object into a human readable form. + + By default, this will fallback to `Kernel.to_string/1` + """ + + @fallback_to_any true + + @type options :: [] + + @spec display(term) :: String.t() | nil + @spec display(term, options) :: String.t() | nil + def display(data, options \\ []) +end + +defimpl C3P0.Humanize, for: Any do + def display(data, _ \\ []), do: to_string(data) +end diff --git a/lib/c3p0/id.ex b/lib/c3p0/id.ex new file mode 100644 index 0000000..37f4665 --- /dev/null +++ b/lib/c3p0/id.ex @@ -0,0 +1,78 @@ +defprotocol C3P0.ID do + @moduledoc """ + Formalizes fetching the ID from data. + + For maps the keys `id`, `"id"` are considered id fields and `guid`, `"guid"` are considered guid fields. + + When requesting a guid, if one cannot be found by default it will fall back to the id. + + ## Using with your own structs + + By default your own structs will behave the same way as a map. + However if you need to redefine which field should be considered the id/guid fields you'll need to derive the protocol. + + ```elixir + defmodule MyStruct do + @derive {C3P0.ID, id_field: :token, guid_field: :arn} + defstruct [:token, :arn, :name] + end + ``` + """ + + @fallback_to_any true + + @doc "Find the id of a piece of data" + @spec id(term) :: binary | nil + def id(data) + + @doc "Find a global id for a piece of data" + @spec guid(term) :: binary | nil + def guid(data) +end + +defimpl C3P0.ID, for: [PID, Reference] do + def id(pid), do: pid + def guid(pid), do: pid +end + +defimpl C3P0.ID, for: Any do + defmacro __deriving__(module, _struct, options) do + quote do + defimpl C3P0.ID, for: unquote(module) do + opts = unquote(options) + id_field = Keyword.get(opts, :id_field) + guid_field = Keyword.get(opts, :guid_field) || id_field + + unless id_field, do: raise "id field not provided to derive #{unquote(module)}" + + @id_field id_field + @guid_field guid_field + + def id(item), do: Map.get(item, @id_field) + def guid(item), do: Map.get(item, @guid_field) + end + end + end + + def id(id) when is_binary(id), do: id + def id(id) when is_atom(id), do: id + def id(id) when is_number(id), do: id + def id(%{id: id}), do: id + def id(%{"id" => id}), do: id + + def id(%{uuid: id}), do: id + def id(%{"uuid" => id}), do: id + + def id(%{guid: id}), do: id + def id(%{"guid" => id}), do: id + def id(%{}), do: nil + def id(v) do + raise Protocol.UndefinedError, protocol: C3P0.ID, value: v, description: "unknown value for id" + end + + def guid(%{guid: id}), do: id + def guid(%{"guid" => id}), do: id + def guid(%{uuid: id}), do: id + def guid(%{"uuid" => id}), do: id + def guid(id), do: C3P0.ID.id(id) +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..2e654f6 --- /dev/null +++ b/mix.exs @@ -0,0 +1,61 @@ +defmodule C3P0.MixProject do + use Mix.Project + + @version "0.1.0" + + def project do + [ + app: :c3p0, + version: "0.1.0", + elixir: "~> 1.9", + description: description(), + consolidate_protocols: Mix.env() != :test, + start_permanent: Mix.env() == :prod, + elixirc_paths: elixirc_paths(Mix.env()), + deps: deps(), + docs: [ + canonical: "https://hexdocs.pm/c3p0", + extras: ["README.md"], + source_ref: "v#{@version}", + source_url: "https://github.com/hassox/c3p0", + ], + package: [ + files: [ + ".formatter.exs", + "mix.exs", + "README.md", + "lib", + ], + licenses: ["MIT"], + links: %{"Github" => "https://github.com/hassox/c3p0"}, + maintainers: ["Daniel Neighman"] + ], + source_url: "https://github.com/hassox/c3p0", + version: @version, + + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + def elixirc_paths(:test), do: ["lib", "test/support"] + def elixirc_paths(_), do: ["lib"] + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + {:ex_doc, "~> 0.21", only: :dev, runtime: false}, + ] + end + + defp description do + """ + C3P0 is a collection of protocols designed to facilitate easy data manipulation. + """ + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..68e8997 --- /dev/null +++ b/mix.lock @@ -0,0 +1,8 @@ +%{ + "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, + "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm"}, + "ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, + "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, + "nimble_parsec": {:hex, :nimble_parsec, "0.5.2", "1d71150d5293d703a9c38d4329da57d3935faed2031d64bc19e77b654ef2d177", [:mix], [], "hexpm"}, +} diff --git a/test/c3p0/blank_test.exs b/test/c3p0/blank_test.exs new file mode 100644 index 0000000..e3c6b7d --- /dev/null +++ b/test/c3p0/blank_test.exs @@ -0,0 +1,96 @@ +defmodule C3P0.BlankTest do + use ExUnit.Case, async: true + + alias C3P0.Blank + alias C3P0.Test.BlankStruct + + test "strings" do + str = "" + assert Blank.blank?(str) == true + assert Blank.present?(str) == false + assert Blank.presence(str) == nil + + str = "n" + assert Blank.blank?(str) == false + assert Blank.present?(str) == true + assert Blank.presence(str) == str + end + + test "nil" do + assert Blank.blank?(nil) == true + assert Blank.present?(nil) == false + assert Blank.presence(nil) == nil + end + + test "binaries" do + value = <<>> + assert Blank.blank?(value) == true + assert Blank.present?(value) == false + assert Blank.presence(value) == nil + + value = <<1>> + assert Blank.blank?(value) == false + assert Blank.present?(value) == true + assert Blank.presence(value) == value + end + + test "lists" do + value = [] + assert Blank.blank?(value) == true + assert Blank.present?(value) == false + assert Blank.presence(value) == nil + + value = [:a] + assert Blank.blank?(value) == false + assert Blank.present?(value) == true + assert Blank.presence(value) == value + end + + test "maps" do + value = %{} + assert Blank.blank?(value) == true + assert Blank.present?(value) == false + assert Blank.presence(value) == nil + + value = %{foo: :bar} + assert Blank.blank?(value) == false + assert Blank.present?(value) == true + assert Blank.presence(value) == value + end + + test "tuples" do + value = {} + assert Blank.blank?(value) == true + assert Blank.present?(value) == false + assert Blank.presence(value) == nil + + value = {:foo, 1} + assert Blank.blank?(value) == false + assert Blank.present?(value) == true + assert Blank.presence(value) == value + end + + test "MapSet" do + value = MapSet.new() + assert Blank.blank?(value) == true + assert Blank.present?(value) == false + assert Blank.presence(value) == nil + + value = MapSet.new([:a]) + assert Blank.blank?(value) == false + assert Blank.present?(value) == true + assert Blank.presence(value) == value + end + + test "struct" do + value = %BlankStruct{} + assert Blank.blank?(value) == true + assert Blank.present?(value) == false + assert Blank.presence(value) == nil + + value = %BlankStruct{name: "not blank"} + assert Blank.blank?(value) == false + assert Blank.present?(value) == true + assert Blank.presence(value) == value + end +end diff --git a/test/c3p0/id_test.exs b/test/c3p0/id_test.exs new file mode 100644 index 0000000..d56cfcc --- /dev/null +++ b/test/c3p0/id_test.exs @@ -0,0 +1,94 @@ +defmodule C3P0.IDTest do + use ExUnit.Case, async: true + alias C3P0.ID + alias C3P0.Test.{IDStruct, FullIDStruct, UnknownIDStruct, UnimplementedStruct} + + describe "id/1" do + test "map" do + assert ID.id(%{id: "id"}) == "id" + assert ID.id(%{"id" => "id"}) == "id" + assert ID.id(%{guid: "id"}) == "id" + assert ID.id(%{"guid" => "id"}) == "id" + assert ID.id(%{"not-an-id" => "id"}) == nil + assert ID.id(%{id: "id", guid: "guid"}) == "id" + assert ID.id(%{"id" => "id", guid: "guid"}) == "id" + assert ID.id(%{"id" => "id", "guid" => "guid"}) == "id" + end + + test "string" do + assert ID.id("id") == "id" + assert ID.id("here") == "here" + assert ID.id(<<1>>) == <<1>> + end + + test "atom" do + assert ID.id(:foo) == :foo + assert ID.id(Here.Is.A.Thing) == Here.Is.A.Thing + assert ID.id(true) == true + assert ID.id(false) == false + assert ID.id(nil) == nil + end + + test "number" do + assert ID.id(123) == 123 + assert ID.id(1.23) == 1.23 + assert ID.id(0) == 0 + end + + test "pid" do + assert ID.id(self()) == self() + end + + test "struct" do + assert ID.id(%IDStruct{token: "the-id"}) == "the-id" + assert ID.id(%FullIDStruct{token: "the-id"}) == "the-id" + assert ID.id(%{__struct__: UnknownIDStruct, unknown_id: "the-id"}) == "the-id" + assert ID.id(%UnimplementedStruct{id: "the-id", guid: "the-guid"}) == "the-id" + end + end + + describe "guid/1" do + test "map" do + assert ID.guid(%{id: "id"}) == "id" + assert ID.guid(%{"id" => "id"}) == "id" + assert ID.guid(%{guid: "id"}) == "id" + assert ID.guid(%{"guid" => "id"}) == "id" + assert ID.guid(%{"not-an-id" => "id"}) == nil + assert ID.guid(%{id: "id", guid: "guid"}) == "guid" + assert ID.guid(%{"id" => "id", guid: "guid"}) == "guid" + assert ID.guid(%{"id" => "id", "guid" => "guid"}) == "guid" + end + + test "string" do + assert ID.guid("id") == "id" + assert ID.guid("here") == "here" + assert ID.guid(<<1>>) == <<1>> + end + + test "atom" do + assert ID.guid(:foo) == :foo + assert ID.guid(Here.Is.A.Thing) == Here.Is.A.Thing + assert ID.guid(true) == true + assert ID.guid(false) == false + assert ID.guid(nil) == nil + end + + test "number" do + assert ID.guid(123) == 123 + assert ID.guid(1.23) == 1.23 + assert ID.guid(0) == 0 + end + + test "pid" do + assert ID.guid(self()) == self() + end + + test "struct" do + assert ID.guid(%IDStruct{token: "the-id"}) == "the-id" + assert ID.guid(%FullIDStruct{token: "the-id", the_guid: "the-guid"}) == "the-guid" + assert ID.guid(%{__struct__: UnknownIDStruct, unknown_id: "the-id", unknown_guid: "guid"}) == "guid" + assert ID.guid(%UnimplementedStruct{id: "the-id", guid: "the-guid"}) == "the-guid" + end + + end +end diff --git a/test/c3p0_test.exs b/test/c3p0_test.exs new file mode 100644 index 0000000..c264175 --- /dev/null +++ b/test/c3p0_test.exs @@ -0,0 +1,4 @@ +defmodule C3P0Test do + use ExUnit.Case + doctest C3P0 +end diff --git a/test/support/blank.ex b/test/support/blank.ex new file mode 100644 index 0000000..fc2db16 --- /dev/null +++ b/test/support/blank.ex @@ -0,0 +1,3 @@ +defmodule C3P0.Test.BlankStruct do + defstruct [:name] +end diff --git a/test/support/id.ex b/test/support/id.ex new file mode 100644 index 0000000..beb8a3a --- /dev/null +++ b/test/support/id.ex @@ -0,0 +1,18 @@ +defmodule C3P0.Test.IDStruct do + @derive {C3P0.ID, id_field: :token} + defstruct [:token] +end + +defmodule C3P0.Test.FullIDStruct do + @derive {C3P0.ID, id_field: :token, guid_field: :the_guid} + defstruct [:token, :the_guid] +end + +defimpl C3P0.ID, for: C3P0.Test.UnknownIDStruct do + def id(%{unknown_id: id}), do: id + def guid(%{unknown_guid: id}), do: id +end + +defmodule C3P0.Test.UnimplementedStruct do + defstruct [:id, :guid] +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()