├── test ├── test_helper.exs └── condiment_test.exs ├── .formatter.exs ├── .gitignore ├── .github └── workflows │ └── ci.yml ├── mix.exs ├── LICENSE ├── mix.lock ├── lib └── condiment.ex └── README.md /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | condiment-*.tar 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [pull_request, push] 3 | jobs: 4 | mix_test: 5 | name: mix test (Elixir ${{ matrix.elixir }} OTP ${{ matrix.otp }}) 6 | strategy: 7 | matrix: 8 | elixir: ["1.7.4", "1.9.1"] 9 | include: 10 | - elixir: "1.7.4" 11 | otp: "20.3.8.23" 12 | - elixir: "1.9.1" 13 | otp: "22.x" 14 | runs-on: ubuntu-16.04 15 | steps: 16 | - uses: actions/checkout@v1 17 | - uses: actions/setup-elixir@v1.0.0 18 | with: 19 | otp-version: ${{ matrix.otp }} 20 | elixir-version: ${{ matrix.elixir }} 21 | - name: Configure git with fake data 22 | run: | 23 | git config --global user.email "you@example.com" 24 | git config --global user.name "Your Name" 25 | - name: Install Dependencies 26 | run: mix deps.get 27 | - name: Check Formatted 28 | run: mix format --check-formatted 29 | - name: Run Tests 30 | run: mix test 31 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Condiment.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :condiment, 7 | version: "0.1.2", 8 | elixir: "~> 1.7", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | name: "Condiment", 12 | description: description(), 13 | package: package(), 14 | source_url: "https://github.com/edisonywh/condiment" 15 | ] 16 | end 17 | 18 | # Run "mix help compile.app" to learn about applications. 19 | def application do 20 | [ 21 | extra_applications: [:logger] 22 | ] 23 | end 24 | 25 | # Run "mix help deps" to learn about dependencies. 26 | defp deps do 27 | [ 28 | {:ex_doc, "~> 0.14", only: :dev, runtime: false} 29 | ] 30 | end 31 | 32 | defp description do 33 | "🍡 Add flavors to your context function without the hassles." 34 | end 35 | 36 | defp package do 37 | [ 38 | maintainers: ["Edison Yap"], 39 | licenses: ["MIT"], 40 | links: %{GitHub: "https://github.com/edisonywh/condiment"} 41 | ] 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2020] [Edison Yap] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"}, 3 | "ex_doc": {:hex, :ex_doc, "0.22.2", "03a2a58bdd2ba0d83d004507c4ee113b9c521956938298eba16e55cc4aba4a6c", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "cf60e1b3e2efe317095b6bb79651f83a2c1b3edcb4d319c421d7fcda8b3aff26"}, 4 | "makeup": {:hex, :makeup, "1.0.3", "e339e2f766d12e7260e6672dd4047405963c5ec99661abdc432e6ec67d29ef95", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "2e9b4996d11832947731f7608fed7ad2f9443011b3b479ae288011265cdd3dad"}, 5 | "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"}, 6 | "nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"}, 7 | } 8 | -------------------------------------------------------------------------------- /lib/condiment.ex: -------------------------------------------------------------------------------- 1 | defmodule Condiment do 2 | @moduledoc """ 3 | Add flavors to your context APIs easily! 4 | 5 | No need to create different functions to cater to different use cases, instead you can have one single public function and add flavors conditionally, with Condiment. 6 | """ 7 | 8 | @type t :: %__MODULE__{ 9 | token: any(), 10 | opts: list(), 11 | resolvers: list(), 12 | condiment_opts: list() 13 | } 14 | 15 | defstruct [:token, :opts, :resolvers, :condiment_opts] 16 | 17 | defmodule NotImplementedError do 18 | defexception [:message] 19 | end 20 | 21 | defimpl Inspect do 22 | import Inspect.Algebra 23 | 24 | def inspect(condiment, _opts) do 25 | data = condiment |> Map.from_struct() |> Enum.into([]) 26 | concat(["#Condiment<", inspect(data), ">"]) 27 | end 28 | end 29 | 30 | @doc """ 31 | To use `Condiment`, you start with the `Condiment.new/2,3` interface. 32 | 33 | The currently available options for `condiment_opts` is: 34 | - `:on_unknown_fields 35 | one of `:nothing`, `:error`, or `:raise` (default). This option specify what to do when user supplies a field that's not resolvable. 36 | """ 37 | @spec new(any, list(), list()) :: Condiment.t() 38 | def new(token, opts, condiment_opts \\ []) do 39 | %__MODULE__{ 40 | token: token, 41 | opts: opts, 42 | resolvers: [], 43 | condiment_opts: condiment_opts 44 | } 45 | end 46 | 47 | @doc """ 48 | `field` is what you allow users to query for. The resolver is how to resolve that query. 49 | 50 | The resolver has to be 2-arity, the first argument is the the result of the previously ran resolver (the first resolver gets `token` instead). 51 | """ 52 | @spec add(Condiment.t(), atom, (any, map() -> any)) :: Condiment.t() 53 | def add(%__MODULE__{} = condiment, key, resolver) 54 | when is_function(resolver, 2) and is_atom(key) do 55 | %{condiment | resolvers: [{key, resolver} | condiment.resolvers]} 56 | end 57 | 58 | @doc """ 59 | Runs all of the resolvers conditionally based on what user requested, it runs in the order that you defined (not the order the user supplied). 60 | """ 61 | @spec run(Condiment.t()) :: any 62 | def run(%__MODULE__{ 63 | token: token, 64 | opts: opts, 65 | resolvers: resolvers, 66 | condiment_opts: condiment_opts 67 | }) do 68 | resolvable_fields = Keyword.keys(resolvers) 69 | requested_fields = Keyword.keys(opts) 70 | 71 | validation = validate_opts(opts, resolvers) 72 | unknown_fields_strategy = Keyword.get(condiment_opts, :on_unknown_fields, :raise) 73 | 74 | case {validation, unknown_fields_strategy} do 75 | {{:error, field}, :error} -> 76 | {:error, build_message(field, resolvable_fields)} 77 | 78 | {{:error, field}, :raise} -> 79 | raise NotImplementedError, build_message(field, resolvable_fields) 80 | 81 | _ -> 82 | map_opts = opts |> Enum.into(%{}) 83 | 84 | resolvers 85 | |> Enum.reverse() 86 | |> Enum.filter(fn {field, _resolver} -> 87 | field in requested_fields 88 | end) 89 | |> Enum.reduce(token, fn {_field, resolver}, acc -> 90 | resolver.(acc, map_opts) 91 | end) 92 | end 93 | end 94 | 95 | defp validate_opts(opts, resolvers) do 96 | resolvable_fields = Keyword.keys(resolvers) 97 | requested_fields = Keyword.keys(opts) 98 | 99 | requested_fields 100 | |> Enum.reduce_while(:ok, fn f, _acc -> 101 | case f in resolvable_fields do 102 | true -> 103 | {:cont, :ok} 104 | 105 | false -> 106 | {:halt, {:error, f}} 107 | end 108 | end) 109 | end 110 | 111 | defp build_message(field, resolvable_fields) do 112 | "Don't know how to resolve #{inspect(field)}. You can add to #{__MODULE__} with `#{__MODULE__}.add/3`. Current known resolvables: #{ 113 | inspect(resolvable_fields) 114 | }" 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /test/condiment_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CondimentTest do 2 | use ExUnit.Case 3 | doctest Condiment 4 | 5 | describe "Condiment.new/2" do 6 | test "should always return %Condiment{}" do 7 | condiment = create_condiment() 8 | 9 | assert %Condiment{token: _token, opts: _opts, resolvers: _resolvers} = condiment 10 | end 11 | 12 | test "token, opts are set correctly" do 13 | token = %{a: 1, b: 2, c: 3} 14 | opts = [c: 8] 15 | 16 | condiment = create_condiment(token: token, opts: opts) 17 | 18 | assert condiment.token == token 19 | assert condiment.opts == opts 20 | assert condiment.resolvers == [] 21 | end 22 | end 23 | 24 | describe "Condiment.add/3" do 25 | test "should set resolvers correctly" do 26 | token = %{a: 1, b: 2, c: 3} 27 | opts = [c: 8] 28 | 29 | condiment = create_condiment(token: token, opts: opts) 30 | 31 | condiment = 32 | condiment 33 | |> Condiment.add(:field1, fn _token, _requests -> nil end) 34 | |> Condiment.add(:field2, fn _token, _requests -> nil end) 35 | 36 | assert is_list(condiment.resolvers) == true 37 | assert length(condiment.resolvers) == 2 38 | end 39 | end 40 | 41 | describe "Condiment.run/1" do 42 | test "final result is the result of the last resolver" do 43 | token = %{} 44 | opts = [first: 1, second: 2] 45 | 46 | condiment = create_condiment(token: token, opts: opts) 47 | 48 | condiment = 49 | condiment 50 | |> Condiment.add(:first, fn token, %{first: first} -> 51 | Map.put(token, :field1, first) 52 | end) 53 | 54 | condiment1 = 55 | condiment |> Condiment.add(:second, fn token, _opts -> Map.put(token, :result, :hey) end) 56 | 57 | condiment2 = condiment |> Condiment.add(:second, fn _token, _opts -> nil end) 58 | 59 | assert Condiment.run(condiment1) == %{result: :hey, field1: 1} 60 | assert Condiment.run(condiment2) == nil 61 | end 62 | 63 | test "should run in order of resolvers added, not user-specified" do 64 | token = %{} 65 | opts = [second: 2, first: 1] 66 | 67 | condiment = create_condiment(token: token, opts: opts) 68 | 69 | result = 70 | condiment 71 | |> Condiment.add(:first, fn _token, %{first: first} -> 1 end) 72 | |> Condiment.add(:second, fn _oken, _opts -> 2 end) 73 | |> Condiment.run() 74 | 75 | assert result == 2 76 | end 77 | 78 | test "should run resolvers for requested fields only" do 79 | token = %{} 80 | opts = [first: 1] 81 | 82 | condiment = create_condiment(token: token, opts: opts) 83 | 84 | result = 85 | condiment 86 | |> Condiment.add(:first, fn token, %{first: first} -> 87 | Map.put(token, :field1, first) 88 | end) 89 | |> Condiment.add(:second, fn token, %{second: second} -> 90 | Map.put(token, :from_field_2, second) 91 | end) 92 | |> Condiment.run() 93 | 94 | assert result == %{field1: 1} 95 | end 96 | 97 | test "should run in order" do 98 | token = [] 99 | opts = [second: 2, first: 1] 100 | 101 | condiment = create_condiment(token: token, opts: opts) 102 | 103 | result = 104 | condiment 105 | |> Condiment.add(:first, fn token, %{first: first} -> 106 | token ++ [first] 107 | end) 108 | |> Condiment.add(:second, fn token, %{second: second} -> 109 | token ++ [second] 110 | end) 111 | |> Condiment.run() 112 | 113 | assert result == [1, 2] 114 | end 115 | 116 | test "should return error tuple on unknown field if on_unknown_fields: :error" do 117 | token = [] 118 | opts = [unresolvable_field: true, condiment_opts: [on_unknown_fields: :error]] 119 | 120 | condiment = create_condiment(token: token, opts: opts) 121 | 122 | result = 123 | condiment 124 | |> Condiment.add(:first, fn _token, _ -> nil end) 125 | |> Condiment.add(:second, fn _token, _ -> nil end) 126 | 127 | assert {:error, _message} = Condiment.run(result) 128 | end 129 | 130 | test "should not raise error on unknown field if on_unknown_fields: :nothing" do 131 | token = [] 132 | opts = [unresolvable_field: true, condiment_opts: [on_unknown_fields: :nothing]] 133 | 134 | condiment = create_condiment(token: token, opts: opts) 135 | 136 | result = 137 | condiment 138 | |> Condiment.add(:first, fn _token, _ -> nil end) 139 | |> Condiment.add(:second, fn _token, _ -> nil end) 140 | 141 | assert [] == Condiment.run(result) 142 | end 143 | 144 | test "should raise error on unknown field if on_unknown_fields: :raise" do 145 | token = [] 146 | opts = [unresolvable_field: true, condiment_opts: [on_unknown_fields: :raise]] 147 | 148 | condiment = create_condiment(token: token, opts: opts) 149 | 150 | result = 151 | condiment 152 | |> Condiment.add(:first, fn _token, _ -> nil end) 153 | |> Condiment.add(:second, fn _token, _ -> nil end) 154 | 155 | assert_raise Condiment.NotImplementedError, fn -> 156 | Condiment.run(result) 157 | end 158 | end 159 | end 160 | 161 | defp create_condiment(opts \\ []) do 162 | token = Keyword.get(opts, :token, %{one: 1, two: 2, three: 3}) 163 | opts = Keyword.get(opts, :opts, []) 164 | condiment_opts = Keyword.get(opts, :condiment_opts, []) 165 | 166 | Condiment.new(token, opts, condiment_opts) 167 | end 168 | end 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Condiment 2 | 3 | > Important: I used this in my app [Slick Inbox](http://slickinbox.com/) for awhile but I have since decided to move off it. I find that even though it's easy to see allowed query options, it's pretty difficult to reuse queries. 4 | 5 | > Right now I'm using the traditional `Enum.reduce(options, query, ...` way with a Query module for now (still a single unified API), I am still testing that out, but I might at some point switch back to non-unified API I outlined below (each function does a specific task), so just letting you know that this is not currently in used anymore, so use at your own risk. 6 | 7 | Add flavors to your context function without the hassles. 8 | 9 | No need to create different functions to cater to different use cases, instead you can have one single public function and add flavors conditionally. 10 | 11 | `Condiment` is a very simple library, the API is largely influenced by libraries such as `Ecto.Multi`, `TokenOperator`, `Sage`, `Absinthe` etc. Scroll down to read about why you would use `Condiment`. 12 | 13 | ## Usage 14 | 15 | ### Example 16 | ```elixir 17 | def list_posts(opts \\ []) do 18 | posts_query() # this can be anything you want 19 | |> Condiment.new(opts) 20 | |> Condiment.add(:featured, &featured_query/2) 21 | |> Condiment.add(:user_id, &by_user_query/2) 22 | |> Condiment.run() 23 | |> Repo.all() 24 | end 25 | ``` 26 | 27 | ### Condiment.new(token, opts, condiment_opts \\ []) 28 | To use `Condiment`, you start with the `Condiment.new/2` interface. 29 | 30 | The first argument is the `token`. It will be passed down to each of the condiment you define later on. 31 | 32 | The second argument is the list of keys that Condiment should act on. Typically it's a list of user-supplied fields. 33 | 34 | The third argument is `condiment_opts`, currently available options are: 35 | 36 | - `:on_unknown_fields` - one of `:nothing`, `:error`, or `:raise` (default). This option specify what to do when user supplies a field that's not resolvable. 37 | 38 | ### Condiment.add(condiment, field, resolver) 39 | `field` is what you allow users to query for. The resolver is how to resolve that query. 40 | 41 | The resolver has to be 2-arity, the first argument is the the result of the previously ran resolver (the first resolver gets `token` instead). 42 | 43 | ### Condiment.run(condiment) 44 | Runs all of the resolvers conditionally based on what user requested, it runs in the order that you defined (not the order the user supplied). 45 | 46 | For example, 47 | 48 | ```elixir 49 | def test(opts \\ []) do 50 | token 51 | |> Condiment.new(opts) 52 | |> Condiment.add(:first, &query/2) 53 | |> Condiment.add(:second, &query/2) 54 | |> Condiment.run() 55 | end 56 | ``` 57 | 58 | If the user did this: 59 | 60 | ```elixir 61 | Blog.test(second: true, first: true) 62 | ``` 63 | 64 | Even though `second` is the first in the list, `first` is still going to run first, because of how you added the resolvers. 65 | 66 | ## Why would I use Condiment? 67 | Phoenix helpfully nudges us to group domain logic and separate it from querying layers like controller directly. 68 | 69 | In theory that is great, but in practice, I often see cases where we start adding a bunch of functions in context like this, where we have multiple functions that largely do the same thing, but differ ever so slightly that requires us to add a new function to cover a new use case. 70 | 71 | ```elixir 72 | def list_posts() do 73 | Repo.all(Post) 74 | end 75 | 76 | def list_featured_posts() do 77 | Post 78 | |> where([p], p.featured == true) 79 | |> Repo.all() 80 | end 81 | 82 | def list_posts_by_user(user) do 83 | Post 84 | |> where([p], p.user_id == user_id) 85 | |> Repo.all() 86 | end 87 | 88 | def list_featured_posts_by_user(user_id) do 89 | Post 90 | |> where([p], p.featured == true) 91 | |> where([p], p.user_id == user_id) 92 | |> Repo.all() 93 | end 94 | ``` 95 | 96 | ### Ecto composable queries 97 | 98 | Now, the amazing Ecto allow us to compose our queries, so we can in fact, simplify it to look a lot nicer. 99 | 100 | ```elixir 101 | # We can separate them into different queries 102 | defp posts_query(), do: Post 103 | defp featured_post_query(query, featured), do: query |> where([q], q.featured == ^featured) 104 | defp by_user_query(query, user_id), do: query |> where([q], q.user_id == ^user_id) 105 | 106 | # And then we can use them like so: 107 | def list_posts_by_user(user_id) do 108 | posts_query() 109 | |> by_user_query(user_id) 110 | end 111 | 112 | def list_featured_posts() do 113 | posts_query() 114 | |> featured_post_query(true) 115 | end 116 | 117 | def list_featured_posts_by_user(user_id) do 118 | posts_query() 119 | |> featured_post_query(true) 120 | |> by_user_query(user_id) 121 | end 122 | ``` 123 | 124 | This is great since it allows me to reuse my queries, and is what I've been using, but it still requires me to build different functions for different use cases. 125 | 126 | My ideal scenario would be to have one a single unified interface, so I could query like this: 127 | 128 | ```elixir 129 | Blog.list_posts(user_id: user_id, featured: true) 130 | ``` 131 | 132 | ### Maybe it's `maybe_*`? 133 | One idea that this could work, is with `maybe_*` functions. This is a pattern that I've seen around and I *mostly* like it, an example would look like this: 134 | 135 | ```elixir 136 | def list_posts(opts \\ []) do 137 | Post 138 | |> maybe_featured(opts) 139 | |> maybe_by_user(opts) 140 | end 141 | ``` 142 | 143 | This allows me to have one public interface, and delegate all conditional logic to the `maybe_*` functions, but I dislike this approach for the following reasons: 144 | 145 | - You need to always pass in something to your `maybe_*` functions (opts in this case). 146 | - You don't know what condition the `maybe` is based on. 147 | - You need to dig into each function to see what actually gets applied. 148 | - It is not clear what options you can pass in. 149 | 150 | Enter `Condiment`! 151 | 152 | ### Condiment 153 | 154 | With `Condiment`, you get the best of all the other approaches I mentioned above. Your context function can now look like this: 155 | 156 | ```elixir 157 | def list_posts(opts \\ []) do 158 | posts_query() 159 | |> Condiment.new(opts) 160 | |> Condiment.add(:featured, &featured_query/2) 161 | |> Condiment.add(:user_id, &by_user_query/2) 162 | |> Condiment.run() 163 | |> Repo.all() 164 | end 165 | ``` 166 | 167 | Great thing is, it is immediately obvious what API you have defined (`featured`, `user_id`), you don't need to hop around functions to figure it out. 168 | 169 | `Condiment` conditionally resolve fields for you, based on what your users are asking for, so: 170 | 171 | ```elixir 172 | Blog.list_posts() # returns all posts, skipping Condiment 173 | Blog.list_posts(featured: true) # returns all featured posts 174 | Blog.list_posts(user_id: 1) # returns all posts by user 175 | Blog.list_posts(featured: true, user_id: 1) # returns all featured posts by user 176 | ``` 177 | 178 | ## How does it work? 179 | `Condiment` is nothing but a glorified `Enum.reduce` with condition checks built-in. 180 | 181 | This means your `token` is really just an initial `accumulator` to `Enum.reduce`! 182 | 183 | This allow you to do some cool tricks like: 184 | 185 | - inject default queries 186 | - build up data conditionally 187 | - optimize REST API by resolving only fields that user requested for (like GraphQL) 188 | 189 | ## Why is it named Condiment? 190 | Imagine in a restaurant where chefs cook dishes, different patrons have different taste buds, some prefer extra salt, others crave for extra black pepper. 191 | 192 | One way you can cater to that is to allow patrons to specify `saltyness` level or `black pepper` amount with their order, and the chef can cater to the requests accordingly. This is a lot of work, for example for every customization you want to add, you now need to re-print your menu to tell user about the new available customizable option. 193 | 194 | With condiments, the restaurant can just put an assortment of condiments on the table, and the patrons can decide for themselves how much salt/pepper they want. 195 | 196 | I find that this translates perfectly to what the library is doing - you being the restaurant, put an assortment of condiments (with `Condiment.add/3`), and your patrons can use them however they like. 197 | 198 | Also, because this library *conditionally* adds stuffs into the dish, I thought that sounded quite like `Condiment`, so why not? :) 199 | 200 | ## Installation 201 | 202 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 203 | by adding `condiment` to your list of dependencies in `mix.exs`: 204 | 205 | ```elixir 206 | def deps do 207 | [ 208 | {:condiment, "~> 0.1.0"} 209 | ] 210 | end 211 | ``` 212 | 213 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 214 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 215 | be found at [https://hexdocs.pm/condiment](https://hexdocs.pm/condiment). 216 | 217 | --------------------------------------------------------------------------------