├── .formatter.exs ├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── config └── config.exs ├── lib ├── juice.ex └── juice │ └── expression.ex ├── mix.exs ├── mix.lock └── test ├── juice └── expression_test.exs ├── juice_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | test-lt-otp-23: 13 | runs-on: ubuntu-latest 14 | name: OTP < 23 ${{matrix.otp}} / Elixir ${{matrix.elixir}} 15 | 16 | strategy: 17 | matrix: 18 | otp: [21.x, 22.x] 19 | elixir: [1.7.x, 1.8.x, 1.9.x, 1.10.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v1.0.0 23 | 24 | - name: set environment variables 25 | uses: allenevans/set-env@v1.1.0 26 | with: 27 | CACHE_VERSION: '//TDbCakB+01qQf1' 28 | 29 | - uses: actions/setup-elixir@v1.3.0 30 | with: 31 | otp-version: ${{matrix.otp}} 32 | elixir-version: ${{matrix.elixir}} 33 | 34 | - name: Cache Dependencies 35 | uses: actions/cache@v1 36 | with: 37 | path: deps 38 | key: deps-elixir-${{env.CACHE_VERSION}}-${{matrix.elixir}}-otp-${{matrix.otp}}-${{runner.os}}-${{hashFiles('mix.lock')}}-${{github.ref}} 39 | restore-keys: | 40 | deps-elixir-${{env.CACHE_VERSION}}-${{matrix.elixir}}-otp-${{matrix.otp}}-${{runner.os}}-${{hashFiles('mix.lock')}}-${{github.ref}} 41 | deps-elixir-${{env.CACHE_VERSION}}-${{matrix.elixir}}-otp-${{matrix.otp}}-${{runner.os}}-${{hashFiles('mix.lock')}}- 42 | 43 | - name: Cache Dev Build 44 | uses: actions/cache@v1 45 | with: 46 | path: _build/dev 47 | key: dev-build-elixir-${{env.CACHE_VERSION}}-${{matrix.elixir}}-otp-${{matrix.otp}}-${{runner.os}}-${{hashFiles('mix.lock')}}-${{github.ref}} 48 | restore-keys: | 49 | dev-build-elixir-${{env.CACHE_VERSION}}-${{matrix.elixir}}-otp-${{matrix.otp}}-${{runner.os}}-${{hashFiles('mix.lock')}}-${{github.ref}} 50 | dev-build-elixir-${{env.CACHE_VERSION}}-${{matrix.elixir}}-otp-${{matrix.otp}}-${{runner.os}}-${{hashFiles('mix.lock')}}- 51 | 52 | - name: Cache Test Build 53 | uses: actions/cache@v1 54 | with: 55 | path: _build/test 56 | key: test-build-elixir-${{env.CACHE_VERSION}}-${{matrix.elixir}}-otp-${{matrix.otp}}-${{runner.os}}-${{hashFiles('mix.lock')}}-${{github.ref}} 57 | restore-keys: | 58 | test-build-elixir-${{env.CACHE_VERSION}}-${{matrix.elixir}}-otp-${{matrix.otp}}-${{runner.os}}-${{hashFiles('mix.lock')}}-${{github.ref}} 59 | test-build-elixir-${{env.CACHE_VERSION}}-${{matrix.elixir}}-otp-${{matrix.otp}}-${{runner.os}}-${{hashFiles('mix.lock')}}- 60 | 61 | - name: Install Dependencies 62 | run: mix deps.get 63 | 64 | - name: Test 65 | run: mix test 66 | 67 | - name: Dialyzer 68 | run: mix dialyzer --halt-exit-status 69 | 70 | test-gte-otp-23: 71 | runs-on: ubuntu-latest 72 | name: OTP >= 22 ${{matrix.otp}} / Elixir ${{matrix.elixir}} 73 | 74 | strategy: 75 | matrix: 76 | otp: [23.x] 77 | elixir: [ 1.8.x, 1.9.x, 1.10.x] 78 | 79 | steps: 80 | - uses: actions/checkout@v1.0.0 81 | 82 | - name: set environment variables 83 | uses: allenevans/set-env@v1.1.0 84 | with: 85 | CACHE_VERSION: '//TDbCakB+01qQf1' 86 | 87 | - uses: actions/setup-elixir@v1.3.0 88 | with: 89 | otp-version: ${{matrix.otp}} 90 | elixir-version: ${{matrix.elixir}} 91 | 92 | - name: Cache Dependencies 93 | uses: actions/cache@v1 94 | with: 95 | path: deps 96 | key: deps-elixir-${{env.CACHE_VERSION}}-${{matrix.elixir}}-otp-${{matrix.otp}}-${{runner.os}}-${{hashFiles('mix.lock')}}-${{github.ref}} 97 | restore-keys: | 98 | deps-elixir-${{env.CACHE_VERSION}}-${{matrix.elixir}}-otp-${{matrix.otp}}-${{runner.os}}-${{hashFiles('mix.lock')}}-${{github.ref}} 99 | deps-elixir-${{env.CACHE_VERSION}}-${{matrix.elixir}}-otp-${{matrix.otp}}-${{runner.os}}-${{hashFiles('mix.lock')}}- 100 | 101 | - name: Cache Dev Build 102 | uses: actions/cache@v1 103 | with: 104 | path: _build/dev 105 | key: dev-build-elixir-${{env.CACHE_VERSION}}-${{matrix.elixir}}-otp-${{matrix.otp}}-${{runner.os}}-${{hashFiles('mix.lock')}}-${{github.ref}} 106 | restore-keys: | 107 | dev-build-elixir-${{env.CACHE_VERSION}}-${{matrix.elixir}}-otp-${{matrix.otp}}-${{runner.os}}-${{hashFiles('mix.lock')}}-${{github.ref}} 108 | dev-build-elixir-${{env.CACHE_VERSION}}-${{matrix.elixir}}-otp-${{matrix.otp}}-${{runner.os}}-${{hashFiles('mix.lock')}}- 109 | 110 | - name: Cache Test Build 111 | uses: actions/cache@v1 112 | with: 113 | path: _build/test 114 | key: test-build-elixir-${{env.CACHE_VERSION}}-${{matrix.elixir}}-otp-${{matrix.otp}}-${{runner.os}}-${{hashFiles('mix.lock')}}-${{github.ref}} 115 | restore-keys: | 116 | test-build-elixir-${{env.CACHE_VERSION}}-${{matrix.elixir}}-otp-${{matrix.otp}}-${{runner.os}}-${{hashFiles('mix.lock')}}-${{github.ref}} 117 | test-build-elixir-${{env.CACHE_VERSION}}-${{matrix.elixir}}-otp-${{matrix.otp}}-${{runner.os}}-${{hashFiles('mix.lock')}}- 118 | 119 | - name: Install Dependencies 120 | run: mix deps.get 121 | 122 | - name: Test 123 | run: mix test 124 | 125 | - name: Dialyzer 126 | run: mix dialyzer --halt-exit-status 127 | -------------------------------------------------------------------------------- /.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 3rd-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 | juice-*.tar 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alex Kwiatkowski 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Juice 2 | 3 | [![Build Status](https://github.com/rupurt/juice/workflows/test/badge.svg?branch=master)](https://github.com/rupurt/juice/actions?query=workflow%3Atest) 4 | [![hex.pm version](https://img.shields.io/hexpm/v/juice.svg?style=flat)](https://hex.pm/packages/juice) 5 | 6 | Reduce in memory data structures using a lightweight query language 7 | 8 | ## Installation 9 | 10 | Add `juice` to your list of dependencies in `mix.exs` 11 | 12 | ```elixir 13 | def deps do 14 | [{:juice, "~> 0.0.3"}] 15 | end 16 | ``` 17 | 18 | ## Usage 19 | 20 | Juice can collect and reject string or atom keys from an Elixir [Map](https://hexdocs.pm/elixir/Map.html) or [List](https://hexdocs.pm/elixir/List.html). 21 | 22 | Given the map 23 | 24 | ```elixir 25 | iex> fruit_basket = %{ 26 | apples: { 27 | granny_smith: 10, 28 | golden_delicious: 3 29 | }, 30 | "oranges" => 5, 31 | plums: 6, 32 | "mangos" => 2, 33 | recipients: [:steph, "michael", :lebron, "charles"] 34 | } 35 | ``` 36 | 37 | Return everything with a wildcard `*` 38 | 39 | ```elixir 40 | iex> Juice.squeeze(fruit_basket, "*") == %{ 41 | apples: { 42 | granny_smith: 10, 43 | golden_delicious: 3 44 | }, 45 | "oranges" => 5, 46 | plums: 6, 47 | "mangos" => 2, 48 | recipients: [:steph, "michael", :lebron, "charles"] 49 | } 50 | ``` 51 | 52 | Remove `plums` and `mangos` 53 | 54 | ```elixir 55 | iex> Juice.squeeze(fruit_basket, "* -plums -mangos") == %{ 56 | apples: { 57 | granny_smith: 10, 58 | golden_delicious: 3 59 | }, 60 | "oranges" => 5, 61 | recipients: [:steph, "michael", :lebron, "charles"] 62 | } 63 | ``` 64 | 65 | Only collect `granny_smith` `apples` and `oranges` with nested `.` notation 66 | 67 | ```elixir 68 | iex> Juice.squeeze(fruit_basket, "apples.granny_smith oranges") == %{ 69 | apples: { 70 | granny_smith: 10, 71 | }, 72 | "oranges" => 5 73 | } 74 | ``` 75 | 76 | Collect `plums` and `mangos` for `charles` 77 | 78 | ```elixir 79 | iex> Juice.squeeze(fruit_basket, "plums mangos recipients.charles") == %{ 80 | plums: 6, 81 | "mangos" => 2, 82 | recipients: ["charles"] 83 | } 84 | ``` 85 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :juice, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:juice, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /lib/juice.ex: -------------------------------------------------------------------------------- 1 | defmodule Juice do 2 | @moduledoc """ 3 | Reduce in memory data structures using a lightweight query language 4 | """ 5 | 6 | alias Juice.Expression 7 | 8 | def squeeze(source, query) when is_bitstring(query) do 9 | expression = Expression.parse(query) 10 | squeeze(source, expression) 11 | end 12 | 13 | def squeeze(source, expression) when is_list(expression) do 14 | expression 15 | |> Enum.reduce( 16 | empty_acc(source), 17 | &eval(source, &1, &2) 18 | ) 19 | end 20 | 21 | defp eval(source, {:+, ["*"]}, _), do: source 22 | defp eval(source, {:-, ["*"]}, _), do: empty_acc(source) 23 | 24 | defp eval(source, {:+, key_chain}, acc) do 25 | collect(key_chain, source, acc) 26 | end 27 | 28 | defp eval(_, {:-, key_chain}, acc) do 29 | reject(key_chain, acc) 30 | end 31 | 32 | defp collect([key | []], source, acc) when is_list(source) and is_list(acc) do 33 | cond do 34 | Enum.member?(source, key) -> 35 | collect_intersection(key, source, acc) 36 | 37 | Enum.member?(source, key |> String.to_atom()) -> 38 | collect_intersection(key |> String.to_atom(), source, acc) 39 | 40 | true -> 41 | acc 42 | end 43 | end 44 | 45 | defp collect([key | []], source, acc) when is_map(source) and is_map(acc) do 46 | key 47 | |> match(source) 48 | |> case do 49 | {:ok, {matched_key, matched_value}} -> 50 | Map.put(acc, matched_key, matched_value) 51 | 52 | {:error, :not_found} -> 53 | acc 54 | end 55 | end 56 | 57 | defp collect([key | tail], source, acc) when is_map(source) and is_map(acc) do 58 | key 59 | |> match(source) 60 | |> case do 61 | {:ok, {matched_key, matched_value}} -> 62 | default_acc = empty_acc(matched_value) 63 | sub_acc = Map.get(acc, matched_key, default_acc) 64 | collected = collect(tail, matched_value, sub_acc) 65 | 66 | Map.put(acc, matched_key, collected) 67 | 68 | {:error, :not_found} -> 69 | acc 70 | end 71 | end 72 | 73 | defp collect_intersection(key, source, acc) do 74 | source_set = MapSet.new(source) 75 | acc_set = MapSet.new([key | acc]) 76 | 77 | source_set 78 | |> MapSet.intersection(acc_set) 79 | |> MapSet.to_list() 80 | end 81 | 82 | defp match(key, source) do 83 | atom_key = String.to_atom(key) 84 | 85 | cond do 86 | Map.has_key?(source, atom_key) -> 87 | {:ok, {atom_key, Map.get(source, atom_key)}} 88 | 89 | Map.has_key?(source, key) -> 90 | {:ok, {key, Map.get(source, key)}} 91 | 92 | true -> 93 | {:error, :not_found} 94 | end 95 | end 96 | 97 | defp reject([key | []], acc) when is_list(acc) do 98 | acc 99 | |> List.delete(key) 100 | |> List.delete(key |> String.to_atom()) 101 | end 102 | 103 | defp reject([key | []], acc) when is_map(acc) do 104 | acc 105 | |> Map.delete(key) 106 | |> Map.delete(key |> String.to_atom()) 107 | end 108 | 109 | defp reject([key | tail], acc) when is_map(acc) do 110 | key 111 | |> match(acc) 112 | |> case do 113 | {:ok, {matched_key, matched_value}} -> 114 | rejected = reject(tail, matched_value) 115 | Map.put(acc, matched_key, rejected) 116 | 117 | {:error, :not_found} -> 118 | acc 119 | end 120 | end 121 | 122 | defp empty_acc(source) when is_map(source), do: %{} 123 | defp empty_acc(source) when is_list(source), do: [] 124 | end 125 | -------------------------------------------------------------------------------- /lib/juice/expression.ex: -------------------------------------------------------------------------------- 1 | defmodule Juice.Expression do 2 | @moduledoc """ 3 | Builds a sequence of operations 4 | """ 5 | 6 | def parse(query) do 7 | query 8 | |> String.split(" ", trim: true) 9 | |> Enum.map(&operation/1) 10 | end 11 | 12 | defp operation("-" <> segment), do: {:-, String.split(segment, ".")} 13 | defp operation(segment), do: {:+, String.split(segment, ".")} 14 | end 15 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Juice.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :juice, 7 | version: "0.0.3", 8 | elixir: "~> 1.7", 9 | package: package(), 10 | description: description(), 11 | start_permanent: Mix.env() == :prod, 12 | deps: deps() 13 | ] 14 | end 15 | 16 | def application do 17 | [ 18 | extra_applications: [:logger] 19 | ] 20 | end 21 | 22 | defp deps do 23 | [ 24 | {:dialyxir, "~> 1.0", only: :dev, runtime: false}, 25 | {:mix_test_watch, "~> 1.0", only: :dev, runtime: false}, 26 | {:ex_unit_notifier, "~> 0.1", only: :test}, 27 | {:ex_doc, ">= 0.0.0", only: :dev} 28 | ] 29 | end 30 | 31 | defp description do 32 | "Reduce in memory data structures using a lightweight query language" 33 | end 34 | 35 | defp package do 36 | %{ 37 | licenses: ["MIT"], 38 | maintainers: ["Alex Kwiatkowski"], 39 | links: %{"GitHub" => "https://github.com/rupurt/juice"} 40 | } 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"}, 3 | "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, 4 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 5 | "ex_doc": {:hex, :ex_doc, "0.21.3", "857ec876b35a587c5d9148a2512e952e24c24345552259464b98bfbb883c7b42", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "0db1ee8d1547ab4877c5b5dffc6604ef9454e189928d5ba8967d4a58a801f161"}, 6 | "ex_unit_notifier": {:hex, :ex_unit_notifier, "0.1.4", "36a2dcab829f506e01bf17816590680dd1474407926d43e64c1263e627c364b8", [:mix], [], "hexpm", "fddf5054dd5fd2f809e837b749570baa5c9798e11d0163921baec49b7d5762f2"}, 7 | "file_system": {:hex, :file_system, "0.2.7", "e6f7f155970975789f26e77b8b8d8ab084c59844d8ecfaf58cbda31c494d14aa", [:mix], [], "hexpm", "b4cfa2d69c7f0b18fd06db222b2398abeef743a72504e6bd7df9c52f171b047f"}, 8 | "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a10c6eb62cca416019663129699769f0c2ccf39428b3bb3c0cb38c718a0c186d"}, 9 | "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"}, 10 | "mix_test_watch": {:hex, :mix_test_watch, "1.0.2", "34900184cbbbc6b6ed616ed3a8ea9b791f9fd2088419352a6d3200525637f785", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "47ac558d8b06f684773972c6d04fcc15590abdb97aeb7666da19fcbfdc441a07"}, 11 | "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"}, 12 | } 13 | -------------------------------------------------------------------------------- /test/juice/expression_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Juice.ExpressionTest do 2 | use ExUnit.Case, async: true 3 | doctest Juice.Expression 4 | 5 | test "builds a list" do 6 | assert Juice.Expression.parse("") == [] 7 | 8 | assert Juice.Expression.parse("* citrus citrus.lemons -citrus.oranges") == [ 9 | {:+, ["*"]}, 10 | {:+, ["citrus"]}, 11 | {:+, ["citrus", "lemons"]}, 12 | {:-, ["citrus", "oranges"]} 13 | ] 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/juice_test.exs: -------------------------------------------------------------------------------- 1 | defmodule JuiceTest do 2 | use ExUnit.Case, async: true 3 | doctest Juice 4 | 5 | setup do 6 | map = %{ 7 | "a" => %{ 8 | "b" => %{"c" => %{"d" => ["A", "B", "C", "D"]}}, 9 | "c" => %{"d" => []}, 10 | "d" => [] 11 | }, 12 | "b" => %{ 13 | "c" => %{"d" => []}, 14 | "d" => [] 15 | }, 16 | "c" => %{"d" => []}, 17 | "d" => [] 18 | } 19 | 20 | {:ok, %{map: map}} 21 | end 22 | 23 | test "returns an empty map when there is no query", %{map: map} do 24 | assert Juice.squeeze(map, "") == %{} 25 | end 26 | 27 | test "returns everything on a wildcard match", %{map: map} do 28 | assert Juice.squeeze(map, "*") == %{ 29 | "a" => %{ 30 | "b" => %{"c" => %{"d" => ["A", "B", "C", "D"]}}, 31 | "c" => %{"d" => []}, 32 | "d" => [] 33 | }, 34 | "b" => %{ 35 | "c" => %{"d" => []}, 36 | "d" => [] 37 | }, 38 | "c" => %{"d" => []}, 39 | "d" => [] 40 | } 41 | end 42 | 43 | test "returns an empty map on a negated wildcard", %{map: map} do 44 | assert Juice.squeeze(map, "* -*") == %{} 45 | end 46 | 47 | test "can nest keys with dot notation", %{map: map} do 48 | assert Juice.squeeze(map, "a") == %{ 49 | "a" => %{ 50 | "b" => %{"c" => %{"d" => ["A", "B", "C", "D"]}}, 51 | "c" => %{"d" => []}, 52 | "d" => [] 53 | } 54 | } 55 | 56 | assert Juice.squeeze(map, "a.b") == %{ 57 | "a" => %{ 58 | "b" => %{"c" => %{"d" => ["A", "B", "C", "D"]}} 59 | } 60 | } 61 | end 62 | 63 | test "merges siblings" do 64 | map = %{ 65 | "a" => %{ 66 | "b" => "B", 67 | "c" => "C", 68 | "d" => "D" 69 | } 70 | } 71 | 72 | assert Juice.squeeze(map, "a.b a.c") == %{ 73 | "a" => %{ 74 | "b" => "B", 75 | "c" => "C" 76 | } 77 | } 78 | end 79 | 80 | test "can negate keys", %{map: map} do 81 | assert Juice.squeeze(map, "a -a") == %{} 82 | assert Juice.squeeze(map, "a.b -a.b") == %{"a" => %{}} 83 | 84 | assert Juice.squeeze(map, "a -a.b -a.c") == %{ 85 | "a" => %{ 86 | "d" => [] 87 | } 88 | } 89 | end 90 | 91 | test "can match keys in a list", %{map: map} do 92 | assert Juice.squeeze(map, "a.b.c.d.A") == %{ 93 | "a" => %{ 94 | "b" => %{"c" => %{"d" => ["A"]}} 95 | } 96 | } 97 | 98 | assert Juice.squeeze(map, "a.b.c.d.A a.b.c.d.B a.b.c.d.C") == %{ 99 | "a" => %{ 100 | "b" => %{"c" => %{"d" => ["A", "B", "C"]}} 101 | } 102 | } 103 | end 104 | 105 | test "can negate keys in a list", %{map: map} do 106 | assert Juice.squeeze(map, "a.b.c.d.A a.b.c.d.B -a.b.c.d.B") == %{ 107 | "a" => %{ 108 | "b" => %{"c" => %{"d" => ["A"]}} 109 | } 110 | } 111 | end 112 | 113 | test "can match on a top level list" do 114 | list = ["a", "b", "c", "d"] 115 | 116 | assert Juice.squeeze(list, "a b") == ["a", "b"] 117 | end 118 | 119 | test "can match and reject symbols" do 120 | list = [:a, :b, :c, :d] 121 | 122 | map = %{ 123 | a: [:alex, :alfred], 124 | b: [:ben, :bonnie], 125 | c: [:charles, :candy] 126 | } 127 | 128 | assert Juice.squeeze(list, "a b") == [:a, :b] 129 | assert Juice.squeeze(list, "* -a -b") == [:c, :d] 130 | 131 | assert Juice.squeeze(map, "a c") == %{ 132 | a: [:alex, :alfred], 133 | c: [:charles, :candy] 134 | } 135 | 136 | assert Juice.squeeze(map, "a.alex b.ben") == %{ 137 | a: [:alex], 138 | b: [:ben] 139 | } 140 | 141 | assert Juice.squeeze(map, "* -b -c") == %{ 142 | a: [:alex, :alfred] 143 | } 144 | end 145 | 146 | test "ignores keys that aren't present" do 147 | list = [:a, :b, :c, :d] 148 | 149 | map = %{ 150 | a: [:alex, :alfred], 151 | b: [:ben, :bonnie], 152 | c: %{ 153 | carrots: 10, 154 | cucumbers: 2 155 | } 156 | } 157 | 158 | assert Juice.squeeze(list, "a z") == [:a] 159 | 160 | assert Juice.squeeze(map, "a z x.y c.carrots c.apples") == %{ 161 | a: [:alex, :alfred], 162 | c: %{ 163 | carrots: 10 164 | } 165 | } 166 | end 167 | end 168 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.configure(formatters: [ExUnit.CLIFormatter, ExUnitNotifier]) 2 | ExUnit.start() 3 | --------------------------------------------------------------------------------