├── .github └── workflows │ └── mix-test.yml ├── .gitignore ├── .tool-versions ├── CHANGELOG.md ├── LICENSE ├── README.md ├── lib └── tiny_maps.ex ├── mix.exs ├── mix.lock └── test ├── test_helper.exs └── tiny_maps_test.exs /.github/workflows/mix-test.yml: -------------------------------------------------------------------------------- 1 | name: Elixir Mix Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - "**" 10 | 11 | env: 12 | MIX_ENV: test 13 | 14 | jobs: 15 | test: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | include: 20 | - elixir: "1.13.4" 21 | otp: "25.3" 22 | - elixir: "1.14.5" 23 | otp: "26.2" 24 | - elixir: "1.15.8" 25 | otp: "26.2" 26 | - elixir: "1.16.3" 27 | otp: "26.2" 28 | - elixir: "1.17.2" 29 | otp: "27.0" 30 | - elixir: "1.18.4" 31 | otp: "28.0.2" 32 | 33 | steps: 34 | - name: Checkout code 35 | uses: actions/checkout@v4 36 | 37 | - name: Set up Elixir 38 | uses: erlef/setup-beam@v1 39 | with: 40 | otp-version: ${{matrix.otp}} 41 | elixir-version: ${{matrix.elixir}} 42 | 43 | - name: Install dependencies 44 | run: mix deps.get 45 | 46 | - name: Cache deps 47 | id: cache-deps 48 | uses: actions/cache@v4 49 | env: 50 | cache-name: cache-elixir-deps 51 | with: 52 | path: deps 53 | key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} 54 | restore-keys: | 55 | ${{ runner.os }}-mix-${{ env.cache-name }}- 56 | 57 | - name: Compile warnings as errors 58 | run: mix compile --warnings-as-errors 59 | 60 | - name: Run tests 61 | run: mix test 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | /doc 6 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | erlang 28.0.2 2 | elixir 1.18.4-otp-26 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [v3.1.0 Unreleased] 9 | 10 | ### Added 11 | 12 | - A new `~K{}` sigil for shortening keyword list definitions. 13 | 14 | ### Changed 15 | 16 | ### Removed 17 | 18 | ## [v3.0.0] 19 | 20 | ### Added 21 | 22 | - Renamed project from `ShorterMaps` to `TinyMaps` 23 | - Add `:logger` to `extra_applications` 24 | 25 | ### Changed 26 | 27 | ### Removed 28 | 29 | - Remvoed `espec` dependency for testing to enable Elixir 1.16 support. 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Chris Meyer 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## TinyMaps 2 | 3 | _A successor to the [shorter_maps](https://github.com/meyercm/shorter_maps) package._ Now with keyword lists! 4 | 5 | `~M` sigil for map shorthand. `~M{a} ~> %{a: a}` 6 | `~K` sigil for keyword list shorthand. `~K{a} ~> [a: a]` 7 | 8 | [![Build Status](https://travis-ci.org/meyercm/shorter_maps.svg?branch=master)](https://travis-ci.org/meyercm/shorter_maps) 9 | 10 | ### Getting started 11 | 12 | 1. Add `{:tiny_maps, "~> 3.0"},` to your mix deps 13 | 2. Add `import TinyMaps` to the top of your module 14 | 3. DRY up your maps and structs with `~M` and `~m`. Instead of `%{name: name}` 15 | use `~M{name}`, and for `%{"name" => name}` use `~m{name}`. When the key and 16 | the variable don't match, don't fret: `~M{name, id: current_id}` expands 17 | to `%{name: name, id: current_id}`. Now use the `~K` sigil to DRY up keyword lists. 18 | 19 | ### Motivation 20 | 21 | Code like `%{id: id, name: name, address: address}` occurs with high frequency 22 | in many programming languages. In Elixir, additional uses occur as we pattern 23 | match to destructure existing maps. 24 | 25 | ES6 provided javascript with a shorthand to create maps with keys inferred by 26 | variable names, and allowed destructuring those maps into variables named for 27 | the keys. `TinyMaps` provides that functionality to Elixir. 28 | 29 | ### Syntax: 30 | 31 | `~M`, `~m`, and `~K` can be used to replace maps **anywhere** in your code. The 32 | `TinyMaps` sigil syntax operates just like a vanilla elixir map, with two 33 | main differences: 34 | 35 | 1. When a variable name stands alone, it is replaced with a key-value pair, 36 | where the key is the variable name as a string (~m) or an atom (~M). The value 37 | will be the variable. For example, `~M{name, id: get_free_id()}` expands to 38 | `%{name: name, id: get_free_id()}`. 39 | 40 | 2. Struct names are enclosed in the sigil, rather than outside, e.g.: 41 | `~M{%StructName key, key2}` === `%StructName{key: key, key2: key2}`. The 42 | struct name must be followed by a space, and then comma-separated keys. 43 | Structs can be updated just like maps: `~M{%StructName old_struct|key_to_update}` 44 | 45 | ### Examples 46 | 47 | ```elixir 48 | iex> import TinyMaps 49 | ...> name = "Chris" 50 | ...> id = 6 51 | ...> ~M{name, id} 52 | %{name: "Chris", id: 6} 53 | 54 | # It's ok to mix in other expressions: 55 | ...> ~M{name, id: id + 200} 56 | %{name: "Chris", id: 206} 57 | 58 | # or even nest the sigil (note the change in delimiters to paren): 59 | ...> ~M{name, id, extra_copy: ~M(name, id)} 60 | %{name: "Chris", id: 6, extra_copy: %{name: "Chris", id: 6}} 61 | 62 | # We can use String keys: 63 | ...> ~m{name, id} 64 | %{"name" => "Chris", "id" => 6} 65 | 66 | # And we can update existing maps: 67 | ...> map_1 = %{name: "Bob", id: 9} 68 | ...> ~M{map_1|name} 69 | %{name: "Chris", id: 9} 70 | 71 | # Struct syntax is a little funky: 72 | ...> defmodule MyStruct do 73 | ...> defstruct [id: nil, name: :default] 74 | ...> end 75 | ...> ~M{%MyStruct id} 76 | %MyStruct{id: 6, name: :default} 77 | 78 | # Structs can be updated too: 79 | ...> initial_struct = %MyStruct{name: "Chris", id: :unknown} 80 | ...> ~M{%MyStruct initial_struct|id} 81 | %MyStruct{name: "Chris", id: 6} 82 | 83 | # Keyword lists can be built with the ~K sigil 84 | ...> a = 1 85 | ...> b = 2 86 | ...> ~K{a, b} 87 | [a: 1, b: 2] 88 | 89 | # And even nested 90 | ...> a = 1 91 | ...> b = 2 92 | ...> ~K{a, b: ~K(b)} 93 | [a: 1, b: [b: 2]] 94 | 95 | # Because the expansion happens at compile time, they can be used __anywhere__: 96 | 97 | # in function heads: 98 | ...> defmodule MyModule do 99 | ...> def my_func(~M{name, _id}), do: {:id_present, name} 100 | ...> def my_func(~M{name}), do: {:no_id, name} 101 | ...> end 102 | 103 | # in pattern matches: 104 | ...> ~M{age, model} = %{age: -30, model: "Delorean", manufacturer: "AMC"} 105 | ...> age 106 | -30 107 | 108 | ``` 109 | 110 | ### Credits 111 | 112 | TinyMaps is continuation of the [ShorterMaps](https://github.com/meyercm/shorter_maps) which has become unmaintained and I have not been able to make contact with the maintainer. 113 | 114 | ShorterMaps added additional features to the original project, `ShortMaps`, 115 | located [here][original-repo]. The reasons for the divergence are summarized 116 | [here][divergent-opinion-issue]. 117 | 118 | [original-repo]: https://github.com/whatyouhide/short_maps 119 | [divergent-opinion-issue]: https://github.com/whatyouhide/short_maps/issues/11 120 | 121 | ### Quick Reference: 122 | 123 | - Atom keys: `~M{a, b}` => `%{a: a, b: b}` 124 | - String keys: `~m{a, b}` => `%{"a" => a, "b" => b}` 125 | - Keyword lists: `~K{a, b, c: d} => [a: a, b: b, c: d]` 126 | - Structs: `~M{%Person id, name}` => `%Person{id: id, name: name}` 127 | - Pinned variables: `~M{^a, b}` => `%{a: ^a, b: b}` 128 | - Ignore matching: `~M{_a, b}` => `%{a: _a, b: b}` 129 | - Map update (strings or atoms): `~M{old|a, b, c}` => `%{old|a: a, b: b, c: c}` 130 | - Struct update: `~M{%Person old_struct|name} => %Person{old_struct|name: name}` 131 | - Mixed mode: `~M{a, b: b_alt}` => `%{a: a, b: b_alt}` 132 | - Expressions: `~M{a, b: a + 1}` => `%{a: a, b: a + 1}` 133 | - Zero-arity: `~M{a, b()}` => `%{a: a, b: b()}` 134 | - Modifiers: `~m{blah}a == ~M{blah}` or `~M{blah}s == ~m{blah}` 135 | 136 | **Note**: you must `import TinyMaps` for the sigils to work. 137 | -------------------------------------------------------------------------------- /lib/tiny_maps.ex: -------------------------------------------------------------------------------- 1 | defmodule TinyMaps do 2 | @readme Path.join(__DIR__, "../README.md") 3 | @external_resource @readme 4 | {:ok, readme_contents} = File.read(@readme) 5 | @moduledoc "#{readme_contents}" 6 | 7 | @default_modifier_m ?s 8 | @default_modifier_M ?a 9 | 10 | @doc """ 11 | Expands to a string keyed map where the keys are a string containing the 12 | variable names, e.g. `~m{name}` expands to `%{"name" => name}`. 13 | 14 | Some common uses of `~m` are when working with JSON and Regex captures, which 15 | use exclusively string keys in their maps. 16 | 17 | # JSON example: 18 | # Here, `~m{name, age}` expands to `%{"name" => name, "age" => age}` 19 | iex> ~m{name, age} = Poison.decode!("{\"name\": \"Chris\",\"age\": \"old\"}") 20 | %{"name" => "Chris", "age" => "old"} 21 | ...> name 22 | "Chris" 23 | ...> age 24 | "old" 25 | 26 | 27 | See the README for extended syntax and usage. 28 | """ 29 | defmacro sigil_m(term, modifiers) 30 | 31 | defmacro sigil_m({:<<>>, fields, [string]}, modifiers) do 32 | case Keyword.get(fields, :line) do 33 | nil -> raise ArgumentError, "interpolation is not supported with the ~m sigil" 34 | line -> do_sigil_m(string, line, modifier(modifiers, @default_modifier_m)) 35 | end 36 | end 37 | 38 | defmacro sigil_m({:<<>>, _, _}, _modifiers) do 39 | raise ArgumentError, "interpolation is not supported with the ~m sigil" 40 | end 41 | 42 | @doc ~S""" 43 | Expands an atom-keyed map with the given keys bound to variables with the same 44 | name. 45 | 46 | Because `~M` operates on atoms, it is compatible with Structs. 47 | 48 | ## Examples: 49 | 50 | # Map construction: 51 | iex> tty = "/dev/ttyUSB0" 52 | ...> baud = 19200 53 | ...> device = ~M{tty, baud} 54 | %{baud: 19200, tty: "/dev/ttyUSB0"} 55 | 56 | # Map Update: 57 | ...> baud = 115200 58 | ...> %{device|baud} 59 | %{baud: 115200, tty: "/dev/ttyUSB0"} 60 | 61 | # Struct Construction 62 | iex> id = 100 63 | ...> ~M{%Person id} 64 | %Person{id: 100, other_key: :default_val} 65 | 66 | """ 67 | 68 | defmacro sigil_M(term, modifiers) 69 | 70 | defmacro sigil_M({:<<>>, fields, [string]}, modifiers) do 71 | case Keyword.get(fields, :line) do 72 | nil -> raise ArgumentError, "interpolation is not supported with the ~M sigil" 73 | line -> do_sigil_m(string, line, modifier(modifiers, @default_modifier_M)) 74 | end 75 | end 76 | 77 | defmacro sigil_M({:<<>>, _, _}, _modifiers) do 78 | raise ArgumentError, "interpolation is not supported with the ~M sigil" 79 | end 80 | 81 | @doc false 82 | defp do_sigil_m("%" <> _rest, _line, ?s) do 83 | raise(ArgumentError, "structs can only consist of atom keys") 84 | end 85 | 86 | defp do_sigil_m(raw_string, line, modifier) do 87 | with {:ok, struct_name, rest} <- get_struct(raw_string), 88 | {:ok, old_map, rest} <- get_old_map(rest), 89 | {:ok, keys_and_values} <- expand_variables(rest, modifier) do 90 | final_string = "%#{struct_name}{#{old_map}#{keys_and_values}}" 91 | # IO.puts("#{raw_string} => #{final_string}") # For debugging expansions gone wrong. 92 | Code.string_to_quoted!(final_string, file: __ENV__.file, line: line) 93 | else 94 | {:error, step, reason} -> 95 | raise(ArgumentError, "TinyMaps parse error in step: #{step}, reason: #{reason}") 96 | end 97 | end 98 | 99 | @doc """ 100 | Expands an atom-keyed keyword list with the given keys bound to variables 101 | with the same name. 102 | 103 | Because `~K` operates on atoms, it is compatible with Structs. 104 | 105 | ## Examples: 106 | 107 | # Keyword List construction: 108 | iex> tty = "/dev/ttyUSB0" 109 | ...> baud = 19200 110 | ...> device = ~K{tty, baud} 111 | [baud: 19200, tty: "/dev/ttyUSB0"] 112 | 113 | # Keyword List with field 114 | iex> id = 100 115 | ...> name = "John" 116 | ...> ~K{id, creator: name} 117 | [id: 100, creator: "John"] 118 | """ 119 | defmacro sigil_K(term, modifiers) 120 | 121 | defmacro sigil_K({:<<>>, fields, [string]}, modifiers) do 122 | case Keyword.get(fields, :line) do 123 | nil -> raise ArgumentError, "interpolation is not supported with the ~K sigil" 124 | line -> do_sigil_K(string, line, modifier(modifiers, @default_modifier_M)) 125 | end 126 | end 127 | 128 | defmacro sigil_K({:<<>>, _, _}, _modifiers) do 129 | raise ArgumentError, "interpolation is not supported with the ~K sigil" 130 | end 131 | 132 | defp do_sigil_K(raw_string, line, modifier) do 133 | with {:ok, _, rest} <- get_struct(raw_string), 134 | {:ok, keys_and_values} <- expand_variables(rest, modifier) do 135 | final_string = "[#{keys_and_values}]" 136 | Code.string_to_quoted!(final_string, file: __ENV__.file, line: line) 137 | else 138 | {:error, step, reason} -> 139 | raise(ArgumentError, "TinyMaps parse error in step: #{step}, reason: #{reason}") 140 | end 141 | end 142 | 143 | @doc false 144 | # expecting something like: '%StructName key1, key2' -or- '%StructName oldmap|key1, key2' 145 | # returns {:ok, old_map, keys_and_vars} | {:ok, "", keys_and_vars} 146 | defp get_struct("%" <> rest) do 147 | [struct_name | others] = String.split(rest, " ") 148 | body = Enum.join(others, " ") 149 | {:ok, struct_name, body} 150 | end 151 | 152 | defp get_struct(no_struct), do: {:ok, "", no_struct} 153 | 154 | @re_prefix "[_^]" 155 | # use ~S to get a real \ 156 | @re_varname ~S"[a-zA-Z0-9_]\w*[?!]?" 157 | @doc false 158 | # expecting something like "old_map|key1, key2" -or- "key1, key2" 159 | # returns {:ok, "#{old_map}|", keys_and_vars} | {:ok, "", keys_and_vars} 160 | defp get_old_map(string) do 161 | cond do 162 | # make sure this is a map update pipe 163 | string =~ ~r/\A\s*#{@re_varname}\s*\|/ -> 164 | [old_map | rest] = String.split(string, "|") 165 | # put back together unintentionally split things 166 | new_body = Enum.join(rest, "|") 167 | {:ok, "#{old_map}|", new_body} 168 | 169 | true -> 170 | {:ok, "", string} 171 | end 172 | end 173 | 174 | @doc false 175 | 176 | # This works simply: split the whole string on commas. check each entry to 177 | # see if it looks like a variable (with or without prefix) or zero-arity 178 | # function. If it is, replace it with the expanded version. Otherwise, leave 179 | # it alone. Once all the pieces are processed, glue it back together with 180 | # commas. 181 | 182 | defp expand_variables(string, modifier) do 183 | result = 184 | string 185 | |> String.split(",") 186 | |> identify_entries() 187 | |> Enum.map(fn s -> 188 | cond do 189 | s =~ ~r/\A\s*#{@re_prefix}?#{@re_varname}(\(\s*\))?\s*\Z/ -> 190 | s 191 | |> String.trim() 192 | |> expand_variable(modifier) 193 | 194 | true -> 195 | s 196 | end 197 | end) 198 | |> Enum.join(",") 199 | 200 | {:ok, result} 201 | end 202 | 203 | @doc false 204 | defp identify_entries(candidates, partial \\ "", acc \\ []) 205 | defp identify_entries([], "", acc), do: acc |> Enum.reverse() 206 | 207 | defp identify_entries([], remainder, _acc) do 208 | # we failed, use code module to raise a syntax error: 209 | Code.string_to_quoted!(remainder) 210 | end 211 | 212 | defp identify_entries([h | t], partial, acc) do 213 | entry = 214 | case partial do 215 | "" -> h 216 | _ -> partial <> "," <> h 217 | end 218 | 219 | if check_entry(entry, [:map, :list]) do 220 | identify_entries(t, "", [entry | acc]) 221 | else 222 | identify_entries(t, entry, acc) 223 | end 224 | end 225 | 226 | @doc false 227 | defp check_entry(_entry, []), do: false 228 | 229 | defp check_entry(entry, [:map | rest]) do 230 | case Code.string_to_quoted("%{#{entry}}") do 231 | {:ok, _} -> true 232 | {:error, _} -> check_entry(entry, rest) 233 | end 234 | end 235 | 236 | defp check_entry(entry, [:list | rest]) do 237 | case Code.string_to_quoted("[#{entry}]") do 238 | {:ok, _} -> true 239 | {:error, _} -> check_entry(entry, rest) 240 | end 241 | end 242 | 243 | @doc false 244 | defp expand_variable(var, ?s) do 245 | "\"#{fix_key(var)}\" => #{var}" 246 | end 247 | 248 | defp expand_variable(var, ?a) do 249 | "#{fix_key(var)}: #{var}" 250 | end 251 | 252 | @doc false 253 | defp fix_key("_" <> name), do: name 254 | defp fix_key("^" <> name), do: name 255 | 256 | defp fix_key(name) do 257 | String.replace_suffix(name, "()", "") 258 | end 259 | 260 | @doc false 261 | defp modifier([], default), do: default 262 | defp modifier([mod], _default) when mod in ~c"as", do: mod 263 | 264 | defp modifier(_, _default) do 265 | raise(ArgumentError, "only these modifiers are supported: s, a") 266 | end 267 | end 268 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule TinyMaps.Mixfile do 2 | use Mix.Project 3 | 4 | @version "3.1.0-rc.1" 5 | @repo_url "https://github.com/abshierjoel/tiny_maps" 6 | 7 | def project do 8 | [ 9 | app: :tiny_maps, 10 | version: @version, 11 | elixir: "~> 1.0", 12 | otp: "~> 21", 13 | build_embedded: Mix.env() == :prod, 14 | start_permanent: Mix.env() == :prod, 15 | deps: deps(), 16 | # Hex 17 | package: hex_package(), 18 | description: "~M sigil for map shorthand. `~M{id, name} ~> %{id: id, name: name}`", 19 | # Docs 20 | name: "TinyMaps" 21 | ] 22 | end 23 | 24 | def application do 25 | [extra_applications: [:logger]] 26 | end 27 | 28 | defp hex_package do 29 | [maintainers: ["Joel Abshier"], licenses: ["MIT"], links: %{"GitHub" => @repo_url}] 30 | end 31 | 32 | defp deps do 33 | [ 34 | {:ex_doc, ">= 0.0.0", only: :dev} 35 | ] 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, 3 | "ex_doc": {:hex, :ex_doc, "0.34.1", "9751a0419bc15bc7580c73fde506b17b07f6402a1e5243be9e0f05a68c723368", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d441f1a86a235f59088978eff870de2e815e290e44a8bd976fe5d64470a4c9d2"}, 4 | "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, 5 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, 6 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, 7 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 8 | } 9 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/tiny_maps_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TinyMapsTest do 2 | alias ExUnit.TestModule 3 | use ExUnit.Case 4 | import TinyMaps 5 | 6 | def eval(quoted_code), do: fn -> Code.eval_quoted(quoted_code) end 7 | 8 | describe "keyword list construction ~K" do 9 | test "with one key" do 10 | key = "value" 11 | 12 | assert [key: "value"] = ~K{key} 13 | end 14 | 15 | test "with multiple key" do 16 | key_1 = "value1" 17 | key_2 = :value2 18 | 19 | assert [key_1: "value1", key_2: :value2] = ~K{key_1, key_2} 20 | end 21 | 22 | test "with mixed keys" do 23 | key_1 = "val1" 24 | key_2_alt = :val2 25 | 26 | assert [key_1: "val1", key_2: :val2] = ~K{key_1, key_2: key_2_alt} 27 | end 28 | 29 | test "raises on invalid varnames" do 30 | quoted = quote do: ~K{4asdf} 31 | assert_raise(SyntaxError, eval(quoted)) 32 | end 33 | end 34 | 35 | describe "map construction ~M" do 36 | test "with one key" do 37 | key = "value" 38 | assert %{key: "value"} = ~M{key} 39 | end 40 | 41 | test "with many keys" do 42 | key_1 = "value_1" 43 | key_2 = :value_2 44 | key_3 = 3 45 | assert %{key_1: "value_1", key_2: :value_2, key_3: 3} = ~M{key_1, key_2, key_3} 46 | end 47 | 48 | test "with mixed keys" do 49 | key_1 = "val_1" 50 | key_2_alt = :val2 51 | assert %{key_1: "val_1", key_2: :val2} = ~M{key_1, key_2: key_2_alt} 52 | end 53 | 54 | test "raises on invalid varnames" do 55 | quoted = quote do: ~M{4asdf} 56 | assert_raise(SyntaxError, eval(quoted)) 57 | end 58 | end 59 | 60 | describe "map construction ~m" do 61 | test "with one key" do 62 | a_key = :test_value 63 | assert %{"a_key" => :test_value} = ~m{a_key} 64 | end 65 | 66 | test "with many keys" do 67 | first_name = "chris" 68 | last_name = "meyer" 69 | 70 | assert %{"first_name" => "chris", "last_name" => "meyer"} = ~m{first_name, last_name} 71 | end 72 | 73 | test "with mixed keys" do 74 | key_1 = "value_1" 75 | key_2_alt = :val_2 76 | 77 | assert %{"key_1" => "value_1", "key_2" => :val_2} = ~m{key_1, "key_2" => key_2_alt} 78 | end 79 | 80 | test "raises on invalid varnames" do 81 | code = quote do: ~m{4asdf} 82 | assert_raise(SyntaxError, eval(code)) 83 | end 84 | end 85 | 86 | describe "inline pattern matches" do 87 | test "for ~M" do 88 | ~M{key_1, key_2} = %{key_1: 1, key_2: 2} 89 | assert 1 = key_1 90 | assert 2 = key_2 91 | end 92 | 93 | test "for ~m" do 94 | ~m{key_1, key_2} = %{"key_1" => 1, "key_2" => 2} 95 | assert 1 = key_1 96 | assert 2 = key_2 97 | end 98 | 99 | test "for ~K" do 100 | ~K{key_1, key_2} = [key_1: 1, key_2: 2] 101 | assert 1 = key_1 102 | assert 2 = key_2 103 | end 104 | 105 | test "with mixed_keys" do 106 | ~M{key_1, key_2: key_2_alt} = %{key_1: :val_1, key_2: "val 2"} 107 | assert :val_1 = key_1 108 | assert "val 2" = key_2_alt 109 | end 110 | 111 | test "fails to match when there is no match" do 112 | code = quote do: ~M{key_1} = %{key_2: 1} 113 | assert_raise(MatchError, eval(code)) 114 | end 115 | end 116 | 117 | describe "function head matches in module" do 118 | defmodule TestModule do 119 | def test(~M{key_1, key_2}), do: {:first, key_1, key_2} 120 | def test(~m{key_1}), do: {:second, key_1} 121 | def test(~K[key_1: key_1, key_2: key_2]), do: {:third, key_1, key_2} 122 | def test(_), do: :third 123 | end 124 | 125 | test "matches in module function heads" do 126 | assert {:first, 1, 2} = TestModule.test(%{key_1: 1, key_2: 2}) 127 | assert {:second, 1} = TestModule.test(%{"key_1" => 1}) 128 | assert {:third, 1, 2} = TestModule.test(key_1: 1, key_2: 2) 129 | end 130 | end 131 | 132 | describe "function head matches in anonymous functions" do 133 | test "matches anonymous function heads" do 134 | fun = fn 135 | ~m{foo} -> {:first, foo} 136 | ~M{foo} -> {:second, foo} 137 | ~K{foo} -> {:third, foo} 138 | _ -> :no_match 139 | end 140 | 141 | assert fun.(%{"foo" => "bar"}) == {:first, "bar"} 142 | assert fun.(%{foo: "barr"}) == {:second, "barr"} 143 | assert fun.(foo: "barrr") == {:third, "barrr"} 144 | assert fun.(%{baz: "bong"}) == :no_match 145 | end 146 | end 147 | 148 | describe "struct syntax" do 149 | defmodule TestStruct do 150 | defstruct a: nil 151 | end 152 | 153 | defmodule TestStruct.Child.GrandChild.Struct do 154 | defstruct a: nil 155 | end 156 | 157 | test "of construction" do 158 | a = 5 159 | assert %TestStruct{a: 5} = ~M{%TestStruct a} 160 | end 161 | 162 | test "of alias resolution" do 163 | alias TestStruct, as: TS 164 | a = 3 165 | assert %TS{a: 3} = ~M{%TS a} 166 | end 167 | 168 | test "of child alias resolution" do 169 | alias TestStruct.Child.GrandChild.{Struct} 170 | a = 0 171 | assert %TestStruct.Child.GrandChild.Struct{a: 0} = ~M{%Struct a} 172 | end 173 | 174 | test "of case pattern-match" do 175 | a = 5 176 | 177 | case %TestStruct{a: 0} do 178 | ~M{%TestStruct ^a} -> raise("shouldn't have matched") 179 | ~M{%TestStruct _a} -> :ok 180 | end 181 | end 182 | 183 | # TODO: figure out why this test doesn't work. A manual test in a compiled 184 | # .ex does raise a KeyError, but not this one: 185 | # test"raises on invalid keys" do 186 | # code = quote do: b = 5; ~m{%TestStruct b} 187 | # expect eval(code) |> to(raise_exception(KeyError)) 188 | # end 189 | 190 | test "works for a local module" do 191 | defmodule InnerTestStruct do 192 | defstruct a: nil 193 | 194 | def test() do 195 | a = 5 196 | ~M{%__MODULE__ a} 197 | end 198 | end 199 | 200 | # need to use the :__struct__ version due to compile order? 201 | assert %{__struct__: InnerTestStruct, a: 5} = InnerTestStruct.test() 202 | end 203 | end 204 | 205 | describe "update syntax ~M" do 206 | test "with one key" do 207 | initial = %{a: 1, b: 2, c: 3} 208 | a = 10 209 | assert %{a: 10, b: 2, c: 3} = ~M{initial|a} 210 | end 211 | 212 | test "allows homogenous keys" do 213 | initial = %{a: 1, b: 2, c: 3} 214 | {a, b} = {6, 7} 215 | assert %{a: 6, b: 7, c: 3} = ~M{initial|a, b} 216 | end 217 | 218 | test "allows mixed keys" do 219 | initial = %{a: 1, b: 2, c: 3} 220 | {a, d} = {6, 7} 221 | assert %{a: 6, b: 7, c: 3} = ~M{initial|a, b: d} 222 | end 223 | 224 | test "can update a struct" do 225 | old_struct = %Range{first: 1, last: 2, step: 1} 226 | last = 3 227 | %Range{first: 1, last: 3} = ~M{old_struct|last} 228 | end 229 | 230 | defmodule TestStructForUpdate do 231 | defstruct a: 1, b: 2, c: 3 232 | end 233 | 234 | test "of multiple key update" do 235 | old_struct = %TestStructForUpdate{a: 10, b: 20, c: 30} 236 | a = 3 237 | b = 4 238 | assert %TestStructForUpdate{a: 3, b: 4, c: 30} = ~M{old_struct|a, b} 239 | end 240 | end 241 | 242 | describe "update syntax ~m" do 243 | test "with one key" do 244 | initial = %{"a" => 1, "b" => 2, "c" => 3} 245 | a = 10 246 | assert %{"a" => 10, "b" => 2, "c" => 3} = ~m{initial|a} 247 | end 248 | 249 | test "allows homogenous keys" do 250 | initial = %{"a" => 1, "b" => 2, "c" => 3} 251 | {a, b} = {6, 7} 252 | assert %{"a" => 6, "b" => 7, "c" => 3} = ~m{initial|a, b} 253 | end 254 | 255 | test "allows mixed keys" do 256 | initial = %{"a" => 1, "b" => 2, "c" => 3} 257 | {a, d} = {6, 7} 258 | assert %{"a" => 6, "b" => 7, "c" => 3} = ~m{initial|a, "b" => d} 259 | end 260 | end 261 | 262 | describe "pin syntax ~K" do 263 | test "matching pin" do 264 | matching = 5 265 | ~K{^matching} = [matching: 5] 266 | end 267 | 268 | test "non-matching pin" do 269 | not_matching = 5 270 | 271 | case Keyword.new(not_matching: 6) do 272 | ~K{^not_matching} -> raise("matched when testshouldn't have") 273 | _ -> :ok 274 | end 275 | end 276 | end 277 | 278 | describe "pin syntax ~M" do 279 | test "happy case" do 280 | matching = 5 281 | ~M{^matching} = %{matching: 5} 282 | end 283 | 284 | test "sad case" do 285 | not_matching = 5 286 | 287 | case %{not_matching: 6} do 288 | ~M{^not_matching} -> raise("matched when testshouldn't have") 289 | _ -> :ok 290 | end 291 | end 292 | end 293 | 294 | describe "pin syntax ~m" do 295 | test "happy case" do 296 | matching = 5 297 | ~m{^matching} = %{"matching" => 5} 298 | end 299 | 300 | test "sad case" do 301 | not_matching = 5 302 | 303 | case %{"not_matching" => 6} do 304 | ~m{^not_matching} -> raise("matched when testshouldn't have") 305 | _ -> :ok 306 | end 307 | end 308 | end 309 | 310 | describe "ignore syntax ~K" do 311 | test "ignore one key" do 312 | ~K{_ignored, real_val} = [ignored: 5, real_val: 19] 313 | assert 19 = real_val 314 | end 315 | 316 | test "ignore multiple keys" do 317 | ~K{_ignored, _ignored_other, real_val} = [ignored: 5, ignored_other: 6, real_val: 19] 318 | assert 19 = real_val 319 | end 320 | 321 | test "failed match" do 322 | case Keyword.new(real_val: 19) do 323 | ~K{_not_present, _real_val} -> raise("matched when testshouldn't have") 324 | _ -> :ok 325 | end 326 | end 327 | end 328 | 329 | describe "ignore syntax ~M" do 330 | test "happy case" do 331 | ~M{_ignored, real_val} = %{ignored: 5, real_val: 19} 332 | assert 19 = real_val 333 | end 334 | 335 | test "sad case" do 336 | case %{real_val: 19} do 337 | ~M{_not_present, _real_val} -> raise("matched when testshouldn't have") 338 | _ -> :ok 339 | end 340 | end 341 | end 342 | 343 | describe "ignore syntax ~m" do 344 | test "happy case" do 345 | ~m{_ignored, real_val} = %{"ignored" => 5, "real_val" => 19} 346 | assert 19 = real_val 347 | end 348 | 349 | test "sad case" do 350 | case %{"real_val" => 19} do 351 | ~m{_not_present, _real_val} -> raise("matched when testshouldn't have") 352 | _ -> :ok 353 | end 354 | end 355 | end 356 | 357 | def blah do 358 | :bleh 359 | end 360 | 361 | describe "zero-arity" do 362 | test "Kernel function" do 363 | assert %{node: node()} == ~M{node()} 364 | end 365 | 366 | test "local function" do 367 | assert %{blah: :bleh} == ~M{blah()} 368 | end 369 | 370 | test "calls the function at run-time" do 371 | mypid = self() 372 | assert %{self: ^mypid} = ~M{self()} 373 | end 374 | end 375 | 376 | describe "nested sigils" do 377 | test "nested ~m inside of ~M" do 378 | a = 1 379 | b = 2 380 | assert %{a: ^a, b: %{"b" => ^b}} = ~M{a, b: ~m(b)} 381 | end 382 | 383 | test "nested ~M inside of ~m" do 384 | a = 1 385 | b = 2 386 | assert %{"a" => ^a, "b" => %{b: ^b}} = ~m{a, "b" => ~M(b)} 387 | end 388 | 389 | test "nested ~K inside of ~M" do 390 | a = 1 391 | b = 2 392 | assert %{a: ^a, b: [b: ^b]} = ~M{a, b: ~K(b)} 393 | end 394 | 395 | test "nested ~M inside of ~K" do 396 | a = 1 397 | b = 2 398 | assert [a: ^a, b: %{b: ^b}] = ~K{a, b: ~M(b)} 399 | end 400 | 401 | test "nested ~K inside of ~K" do 402 | a = 1 403 | b = 2 404 | assert [a: ^a, b: [b: ^b]] = ~K{a, b: ~K(b)} 405 | end 406 | 407 | test "two levels" do 408 | [a, b, c] = [1, 2, 3] 409 | assert %{a: ^a, b: %{b: ^b, c: ^c}} = ~M{a, b: ~M(b, c)} 410 | end 411 | 412 | test "three levels" do 413 | [a, b, c] = [1, 2, 3] 414 | assert %{a: ^a, b: %{b: ^b, c: %{c: ^c}}} = ~M{a, b: ~M(b, c: ~M[c])} 415 | end 416 | end 417 | 418 | describe "literals" do 419 | test "adding" do 420 | a = 1 421 | assert %{a: ^a, b: 3} = ~M{a, b: a+2} 422 | end 423 | 424 | test "function call" do 425 | a = [] 426 | %{a: [], len: 0} = ~M{a, len: length(a)} 427 | end 428 | 429 | test "embedded tinymap" do 430 | a = 1 431 | b = 2 432 | assert %{a: ^a, b: %{b: ^b}} = ~M{a, b: ~M(b)} 433 | end 434 | 435 | test "embedded commas" do 436 | a = 1 437 | assert %{a: ^a, b: <<1, 2, 3>>} = ~M{a, b: <<1, 2, 3>>} 438 | end 439 | 440 | test "function call with arguments" do 441 | a = :hey 442 | assert %{a: ^a, b: 3} = ~M{a, b: div(10, 3)} 443 | end 444 | 445 | test "pipeline" do 446 | a = :hey 447 | assert %{a: ^a, b: "hey"} = ~M{a, b: a |> Atom.to_string} 448 | end 449 | 450 | test "string keys" do 451 | a = "blah" 452 | b = "bleh" 453 | 454 | assert %{"a" => ^a, "b" => %{"a" => ^a, "b" => ^b}} = ~m{a, "b" => ~m(a, b)} 455 | end 456 | 457 | test "string interpolation" do 458 | a = "blah" 459 | b = "bleh" 460 | assert %{a: ^a, b: "blehbleh, c"} = ~M(a, b: "#{b <> b}, c") 461 | end 462 | end 463 | 464 | describe "regressions and bugfixes" do 465 | test "of mixed-mode parse error" do 466 | a = 5 467 | assert %{key: [1, ^a, 2]} = ~M{key: [1, a, 2]} 468 | end 469 | 470 | test "of import shadowing" do 471 | defmodule Test do 472 | import TinyMaps 473 | 474 | def test do 475 | get_struct(:a) 476 | get_old_map(:a) 477 | expand_variables(:a, :b) 478 | expand_variable(:a, :b) 479 | identify_entries(:a, :b, :c) 480 | check_entry(:a, :b) 481 | expand_variable(:a, :b) 482 | fix_key(:a) 483 | modifier(:a, :b) 484 | do_sigil_m(:a, :b) 485 | end 486 | 487 | def get_struct(a), do: ~M{a} 488 | def get_old_map(a), do: a 489 | def expand_variables(a, b), do: {a, b} 490 | def expand_variable(a, b), do: {a, b} 491 | def identify_entries(a, b, c), do: {a, b, c} 492 | def check_entry(a, b), do: {a, b} 493 | def fix_key(a), do: a 494 | def modifier(a, b), do: {a, b} 495 | def do_sigil_m(a, b), do: {a, b} 496 | end 497 | end 498 | 499 | test "of varname variations" do 500 | a? = 1 501 | assert %{a?: ^a?} = ~M{a?} 502 | a5 = 2 503 | assert %{a5: ^a5} = ~M{a5} 504 | a! = 3 505 | assert %{a!: ^a!} = ~M{a!} 506 | end 507 | end 508 | end 509 | --------------------------------------------------------------------------------