From 379708d018bbe9076acd1352b549b3860dd59175 Mon Sep 17 00:00:00 2001 From: Joel Abshier Date: Fri, 28 Jun 2024 16:11:27 +0200 Subject: [PATCH] chore: Rename project to TinyMaps --- README.md | 66 ++++--- lib/{shorter_maps.ex => tiny_maps.ex} | 4 +- mix.exs | 12 +- spec/shorter_maps_spec.exs | 182 +++++++++++------- ...orter_maps_test.exs => tiny_maps_test.exs} | 8 +- 5 files changed, 161 insertions(+), 111 deletions(-) rename lib/{shorter_maps.ex => tiny_maps.ex} (98%) rename test/{shorter_maps_test.exs => tiny_maps_test.exs} (98%) diff --git a/README.md b/README.md index d80c19f..349d894 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -## ShorterMaps +## TinyMaps + +_A successor to the [shorter_maps](https://github.com/meyercm/shorter_maps) package._ `~M` sigil for map shorthand. `~M{a} ~> %{a: a}` @@ -6,9 +8,9 @@ ### Getting started -1) Add `{:shorter_maps, "~> 2.0"},` to your mix deps -2) Add `import ShorterMaps` to the top of your module -3) DRY up your maps and structs with `~M` and `~m`. Instead of `%{name: name}` +1. Add `{:tiny_maps, "~> 3.0"},` to your mix deps +2. Add `import TinyMaps` to the top of your module +3. DRY up your maps and structs with `~M` and `~m`. Instead of `%{name: name}` use `~M{name}`, and for `%{"name" => name}` use `~m{name}`. When the key and the variable don't match, don't fret: `~M{name, id: current_id}` expands to `%{name: name, id: current_id}`. @@ -16,33 +18,33 @@ ### Motivation Code like `%{id: id, name: name, address: address}` occurs with high frequency -in many programming languages. In Elixir, additional uses occur as we pattern +in many programming languages. In Elixir, additional uses occur as we pattern match to destructure existing maps. ES6 provided javascript with a shorthand to create maps with keys inferred by variable names, and allowed destructuring those maps into variables named for -the keys. `ShorterMaps` provides that functionality to Elixir. +the keys. `TinyMaps` provides that functionality to Elixir. ### Syntax: -`~M` and `~m` can be used to replace maps __anywhere__ in your code. The -`ShorterMaps` sigil syntax operates just like a vanilla elixir map, with two +`~M` and `~m` can be used to replace maps **anywhere** in your code. The +`TinyMaps` sigil syntax operates just like a vanilla elixir map, with two main differences: - 1) When a variable name stands alone, it is replaced with a key-value pair, - where the key is the variable name as a string (~m) or an atom (~M). The value - will be the variable. For example, `~M{name, id: get_free_id()}` expands to - `%{name: name, id: get_free_id()}`. +1. When a variable name stands alone, it is replaced with a key-value pair, + where the key is the variable name as a string (~m) or an atom (~M). The value + will be the variable. For example, `~M{name, id: get_free_id()}` expands to + `%{name: name, id: get_free_id()}`. - 2) Struct names are enclosed in the sigil, rather than outside, e.g.: - `~M{%StructName key, key2}` === `%StructName{key: key, key2: key2}`. The - struct name must be followed by a space, and then comma-separated keys. - Structs can be updated just like maps: `~M{%StructName old_struct|key_to_update}` +2. Struct names are enclosed in the sigil, rather than outside, e.g.: + `~M{%StructName key, key2}` === `%StructName{key: key, key2: key2}`. The + struct name must be followed by a space, and then comma-separated keys. + Structs can be updated just like maps: `~M{%StructName old_struct|key_to_update}` ### Examples ```elixir -iex> import ShorterMaps +iex> import TinyMaps ...> name = "Chris" ...> id = 6 ...> ~M{name, id} @@ -94,7 +96,9 @@ iex> import ShorterMaps ### Credits -ShorterMaps adds additional features to the original project, `ShortMaps`, +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. + +ShorterMaps added additional features to the original project, `ShortMaps`, located [here][original-repo]. The reasons for the divergence are summarized [here][divergent-opinion-issue]. @@ -103,16 +107,16 @@ located [here][original-repo]. The reasons for the divergence are summarized ### Quick Reference: -* Atom keys: `~M{a, b}` => `%{a: a, b: b}` -* String keys: `~m{a, b}` => `%{"a" => a, "b" => b}` -* Structs: `~M{%Person id, name}` => `%Person{id: id, name: name}` -* Pinned variables: `~M{^a, b}` => `%{a: ^a, b: b}` -* Ignore matching: `~M{_a, b}` => `%{a: _a, b: b}` -* Map update (strings or atoms): `~M{old|a, b, c}` => `%{old|a: a, b: b, c: c}` -* Struct update: `~M{%Person old_struct|name} => %Person{old_struct|name: name}` -* Mixed mode: `~M{a, b: b_alt}` => `%{a: a, b: b_alt}` -* Expressions: `~M{a, b: a + 1}` => `%{a: a, b: a + 1}` -* Zero-arity: `~M{a, b()}` => `%{a: a, b: b()}` -* Modifiers: `~m{blah}a == ~M{blah}` or `~M{blah}s == ~m{blah}` - -**Note**: you must `import ShorterMaps` for the sigils to work. +- Atom keys: `~M{a, b}` => `%{a: a, b: b}` +- String keys: `~m{a, b}` => `%{"a" => a, "b" => b}` +- Structs: `~M{%Person id, name}` => `%Person{id: id, name: name}` +- Pinned variables: `~M{^a, b}` => `%{a: ^a, b: b}` +- Ignore matching: `~M{_a, b}` => `%{a: _a, b: b}` +- Map update (strings or atoms): `~M{old|a, b, c}` => `%{old|a: a, b: b, c: c}` +- Struct update: `~M{%Person old_struct|name} => %Person{old_struct|name: name}` +- Mixed mode: `~M{a, b: b_alt}` => `%{a: a, b: b_alt}` +- Expressions: `~M{a, b: a + 1}` => `%{a: a, b: a + 1}` +- Zero-arity: `~M{a, b()}` => `%{a: a, b: b()}` +- Modifiers: `~m{blah}a == ~M{blah}` or `~M{blah}s == ~m{blah}` + +**Note**: you must `import TinyMaps` for the sigils to work. diff --git a/lib/shorter_maps.ex b/lib/tiny_maps.ex similarity index 98% rename from lib/shorter_maps.ex rename to lib/tiny_maps.ex index 5600c09..3a3c495 100644 --- a/lib/shorter_maps.ex +++ b/lib/tiny_maps.ex @@ -1,4 +1,4 @@ -defmodule ShorterMaps do +defmodule TinyMaps do @readme Path.join(__DIR__, "../README.md") @external_resource @readme {:ok, readme_contents} = File.read(@readme) @@ -92,7 +92,7 @@ defmodule ShorterMaps do Code.string_to_quoted!(final_string, file: __ENV__.file, line: line) else {:error, step, reason} -> - raise(ArgumentError, "ShorterMaps parse error in step: #{step}, reason: #{reason}") + raise(ArgumentError, "TinyMaps parse error in step: #{step}, reason: #{reason}") end end diff --git a/mix.exs b/mix.exs index 2632f81..cfdac4a 100644 --- a/mix.exs +++ b/mix.exs @@ -1,12 +1,12 @@ -defmodule ShorterMaps.Mixfile do +defmodule TinyMaps.Mixfile do use Mix.Project - @version "2.2.5" - @repo_url "https://github.com/meyercm/shorter_maps" + @version "3.0.0" + @repo_url "https://github.com/abshierjoel/tiny_maps" def project do [ - app: :shorter_maps, + app: :tiny_maps, version: @version, elixir: "~> 1.0", build_embedded: Mix.env() == :prod, @@ -16,7 +16,7 @@ defmodule ShorterMaps.Mixfile do package: hex_package(), description: "~M sigil for map shorthand. `~M{id, name} ~> %{id: id, name: name}`", # Docs - name: "ShorterMaps" + name: "TinyMaps" ] end @@ -25,7 +25,7 @@ defmodule ShorterMaps.Mixfile do end defp hex_package do - [maintainers: ["Chris Meyer"], licenses: ["MIT"], links: %{"GitHub" => @repo_url}] + [maintainers: ["Joel Abshier"], licenses: ["MIT"], links: %{"GitHub" => @repo_url}] end defp deps do diff --git a/spec/shorter_maps_spec.exs b/spec/shorter_maps_spec.exs index 47203f5..0de2d69 100644 --- a/spec/shorter_maps_spec.exs +++ b/spec/shorter_maps_spec.exs @@ -1,6 +1,6 @@ -defmodule ShorterMapsSpec do +defmodule TinyMapsSpec do use ESpec - import ShorterMaps + import TinyMaps def eval(quoted_code), do: fn -> Code.eval_quoted(quoted_code) end @@ -8,42 +8,57 @@ defmodule ShorterMapsSpec do context "~M" do example "with one key" do key = "value" - expect ~M{key}|> to(eq %{key: "value"}) + expect(~M{key} |> to(eq(%{key: "value"}))) end + example "with many keys" do key_1 = "value_1" key_2 = :value_2 key_3 = 3 - expect ~M{key_1, key_2, key_3} |> to(eq %{key_1: "value_1", key_2: :value_2, key_3: 3}) + expect(~M{key_1, key_2, key_3} |> to(eq(%{key_1: "value_1", key_2: :value_2, key_3: 3}))) end + example "with mixed keys" do key_1 = "val_1" key_2_alt = :val2 - expect ~M{key_1, key_2: key_2_alt} |> to(eq %{key_1: "val_1", key_2: :val2}) + expect(~M{key_1, key_2: key_2_alt} |> to(eq(%{key_1: "val_1", key_2: :val2}))) end + it "raises on invalid varnames" do quoted = quote do: ~M{4asdf} - expect fn-> Code.eval_quoted(quoted) end |> to(raise_exception()) + expect(fn -> Code.eval_quoted(quoted) end |> to(raise_exception())) end end + context "~m" do example "with one key" do a_key = :test_value - expect ~m{a_key} |> to(eq %{"a_key" => :test_value}) + expect(~m{a_key} |> to(eq(%{"a_key" => :test_value}))) end + example "with many keys" do first_name = "chris" last_name = "meyer" - expect ~m{first_name, last_name} |> to(eq %{"first_name" => "chris", "last_name" => "meyer"}) + + expect( + ~m{first_name, last_name} + |> to(eq(%{"first_name" => "chris", "last_name" => "meyer"})) + ) end + example "with mixed keys" do key_1 = "value_1" key_2_alt = :val_2 - expect ~m{key_1, "key_2" => key_2_alt} |> to(eq %{"key_1" => "value_1", "key_2" => :val_2}) + + expect( + ~m{key_1, "key_2" => key_2_alt} + |> to(eq(%{"key_1" => "value_1", "key_2" => :val_2})) + ) end + it "raises on invalid varnames" do code = quote do: ~m{4asdf} - expect eval(code) |> to(raise_exception(SyntaxError)) + expect(eval(code) |> to(raise_exception(SyntaxError))) end end end @@ -51,22 +66,25 @@ defmodule ShorterMapsSpec do describe "inline pattern matches" do example "for ~M" do ~M{key_1, key_2} = %{key_1: 1, key_2: 2} - expect key_1 |> to(eq 1) - expect key_2 |> to(eq 2) + expect(key_1 |> to(eq(1))) + expect(key_2 |> to(eq(2))) end + example "for ~m" do ~m{key_1, key_2} = %{"key_1" => 1, "key_2" => 2} - expect key_1 |> to(eq 1) - expect key_2 |> to(eq 2) + expect(key_1 |> to(eq(1))) + expect(key_2 |> to(eq(2))) end + example "with mixed_keys" do ~M{key_1, key_2: key_2_alt} = %{key_1: :val_1, key_2: "val 2"} - expect key_1 |> to(eq :val_1) - expect key_2_alt |> to(eq "val 2") + expect(key_1 |> to(eq(:val_1))) + expect(key_2_alt |> to(eq("val 2"))) end + it "fails to match when there is no match" do code = quote do: ~M{key_1} = %{key_2: 1} - expect eval(code) |> to(raise_exception(MatchError)) + expect(eval(code) |> to(raise_exception(MatchError))) end end @@ -79,18 +97,18 @@ defmodule ShorterMapsSpec do end it "matches in module function heads" do - expect TestModule.test(%{key_1: 1, key_2: 2}) |> to(eq {:first, 1, 2}) - expect TestModule.test(%{"key_1" => 1}) |> to(eq {:second, 1}) + expect(TestModule.test(%{key_1: 1, key_2: 2}) |> to(eq({:first, 1, 2}))) + expect(TestModule.test(%{"key_1" => 1}) |> to(eq({:second, 1}))) end end context "in anonymous functions" do it "matches anonymous function heads" do fun = fn - ~m{foo} -> {:first, foo} - ~M{foo} -> {:second, foo} - _ -> :no_match - end + ~m{foo} -> {:first, foo} + ~M{foo} -> {:second, foo} + _ -> :no_match + end assert fun.(%{"foo" => "bar"}) == {:first, "bar"} assert fun.(%{foo: "barr"}) == {:second, "barr"} @@ -101,28 +119,33 @@ defmodule ShorterMapsSpec do describe "struct syntax" do defmodule TestStruct do - defstruct [a: nil] + defstruct a: nil end + defmodule TestStruct.Child.GrandChild.Struct do - defstruct [a: nil] + defstruct a: nil end + example "of construction" do a = 5 - expect ~M{%TestStruct a} |> to(eq %TestStruct{a: 5}) + expect(~M{%TestStruct a} |> to(eq(%TestStruct{a: 5}))) end example "of alias resolution" do alias TestStruct, as: TS a = 3 - expect ~M{%TS a} |> to(eq %TS{a: 3}) + expect(~M{%TS a} |> to(eq(%TS{a: 3}))) end + example "of alias resolution" do alias TestStruct.Child.GrandChild.{Struct} a = 0 - expect ~M{%Struct a} |> to(eq %TestStruct.Child.GrandChild.Struct{a: 0}) + expect(~M{%Struct a} |> to(eq(%TestStruct.Child.GrandChild.Struct{a: 0}))) end + example "of case pattern-match" do a = 5 + case %TestStruct{a: 0} do ~M{%TestStruct ^a} -> raise("shouldn't have matched") ~M{%TestStruct _a} -> :ok @@ -138,17 +161,17 @@ defmodule ShorterMapsSpec do it "works for a local module" do defmodule InnerTestStruct do - defstruct [a: nil] + defstruct a: nil + def test() do a = 5 ~M{%__MODULE__ a} end end + # need to use the :__struct__ version due to compile order? - expect InnerTestStruct.test |> to(eq %{__struct__: InnerTestStruct, a: 5}) + expect(InnerTestStruct.test() |> to(eq(%{__struct__: InnerTestStruct, a: 5}))) end - - end describe "update syntax" do @@ -156,49 +179,56 @@ defmodule ShorterMapsSpec do example "with one key" do initial = %{a: 1, b: 2, c: 3} a = 10 - expect ~M{initial|a} |> to(eq %{a: 10, b: 2, c: 3}) + expect(~M{initial|a} |> to(eq(%{a: 10, b: 2, c: 3}))) end + it "allows homogenous keys" do initial = %{a: 1, b: 2, c: 3} {a, b} = {6, 7} - expect ~M{initial|a, b} |> to(eq %{a: 6, b: 7, c: 3}) + expect(~M{initial|a, b} |> to(eq(%{a: 6, b: 7, c: 3}))) end + it "allows mixed keys" do initial = %{a: 1, b: 2, c: 3} {a, d} = {6, 7} - expect ~M{initial|a, b: d} |> to(eq %{a: 6, b: 7, c: 3}) + expect(~M{initial|a, b: d} |> to(eq(%{a: 6, b: 7, c: 3}))) end + it "can update a struct" do old_struct = %Range{first: 1, last: 2} last = 3 - expect ~M{old_struct|last} |> to(eq %Range{first: 1, last: 3}) + expect(~M{old_struct|last} |> to(eq(%Range{first: 1, last: 3}))) end + defmodule TestStructForUpdate do - defstruct [a: 1, b: 2, c: 3] + defstruct a: 1, b: 2, c: 3 end example "of multiple key update" do old_struct = %TestStructForUpdate{a: 10, b: 20, c: 30} a = 3 b = 4 - expect ~M{old_struct|a, b} |> to(eq %TestStructForUpdate{a: 3, b: 4, c: 30}) + expect(~M{old_struct|a, b} |> to(eq(%TestStructForUpdate{a: 3, b: 4, c: 30}))) end end + context "~m" do example "with one key" do initial = %{"a" => 1, "b" => 2, "c" => 3} a = 10 - expect ~m{initial|a} |> to(eq %{"a" => 10, "b" => 2, "c" => 3}) + expect(~m{initial|a} |> to(eq(%{"a" => 10, "b" => 2, "c" => 3}))) end + it "allows homogenous keys" do initial = %{"a" => 1, "b" => 2, "c" => 3} {a, b} = {6, 7} - expect ~m{initial|a, b} |> to(eq %{"a" => 6, "b" => 7, "c" => 3}) + expect(~m{initial|a, b} |> to(eq(%{"a" => 6, "b" => 7, "c" => 3}))) end + it "allows mixed keys" do initial = %{"a" => 1, "b" => 2, "c" => 3} {a, d} = {6, 7} - expect ~m{initial|a, "b" => d} |> to(eq %{"a" => 6, "b" => 7, "c" => 3}) + expect(~m{initial|a, "b" => d} |> to(eq(%{"a" => 6, "b" => 7, "c" => 3}))) end end end @@ -209,21 +239,26 @@ defmodule ShorterMapsSpec do matching = 5 ~M{^matching} = %{matching: 5} end + example "sad case" do not_matching = 5 + case %{not_matching: 6} do ~M{^not_matching} -> raise("matched when it shouldn't have") _ -> :ok end end end + context "~m" do example "happy case" do matching = 5 ~m{^matching} = %{"matching" => 5} end + example "sad case" do not_matching = 5 + case %{"not_matching" => 6} do ~m{^not_matching} -> raise("matched when it shouldn't have") _ -> :ok @@ -236,8 +271,9 @@ defmodule ShorterMapsSpec do context "~M" do example "happy case" do ~M{_ignored, real_val} = %{ignored: 5, real_val: 19} - expect real_val |> to(eq 19) + expect(real_val |> to(eq(19))) end + example "sad case" do case %{real_val: 19} do ~M{_not_present, _real_val} -> raise("matched when it shouldn't have") @@ -245,11 +281,13 @@ defmodule ShorterMapsSpec do end end end + context "~m" do example "happy case" do ~m{_ignored, real_val} = %{"ignored" => 5, "real_val" => 19} - expect real_val |> to(eq 19) + expect(real_val |> to(eq(19))) end + example "sad case" do case %{"real_val" => 19} do ~m{_not_present, _real_val} -> raise("matched when it shouldn't have") @@ -264,75 +302,85 @@ defmodule ShorterMapsSpec do describe "zero-arity" do example "Kernel function" do - expect ~M{node()} |> to(eq(%{node: node()})) + expect(~M{node()} |> to(eq(%{node: node()}))) end example "local function" do - expect ~M{blah()} |> to(eq(%{blah: :bleh})) + expect(~M{blah()} |> to(eq(%{blah: :bleh}))) end it "calls the function at run-time" do mypid = self() - expect ~M{self()} |> to(eq(%{self: mypid})) + expect(~M{self()} |> to(eq(%{self: mypid}))) end end describe "nested sigils" do example "two levels" do - [a,b,c] = [1,2,3] - expect ~M{a, b: ~M(b, c)} |> to(eq(%{a: 1, b: %{b: 2, c: 3}})) + [a, b, c] = [1, 2, 3] + expect(~M{a, b: ~M(b, c)} |> to(eq(%{a: 1, b: %{b: 2, c: 3}}))) end end + describe "literals" do example "adding" do a = 1 - expect ~M{a, b: a+2} |> to(eq(%{a: 1, b: 3})) + expect(~M{a, b: a+2} |> to(eq(%{a: 1, b: 3}))) end + example "function call" do a = [] - expect ~M{a, len: length(a)} |> to(eq(%{a: [], len: 0})) + expect(~M{a, len: length(a)} |> to(eq(%{a: [], len: 0}))) end - example "embedded shortermap" do + + example "embedded tinymap" do a = 1 b = 2 - expect ~M{a, b: ~M(b)} |> to(eq(%{a: 1, b: %{b: 2}})) + expect(~M{a, b: ~M(b)} |> to(eq(%{a: 1, b: %{b: 2}}))) end + example "embedded commas" do a = 1 - expect ~M{a, b: <<1, 2, 3>>} |> to(eq(%{a: 1, b: <<1, 2, 3>>})) + expect(~M{a, b: <<1, 2, 3>>} |> to(eq(%{a: 1, b: <<1, 2, 3>>}))) end + example "function call with arguments" do a = :hey - expect ~M{a, b: div(10, 3)} |> to(eq(%{a: :hey, b: 3})) + expect(~M{a, b: div(10, 3)} |> to(eq(%{a: :hey, b: 3}))) end + example "pipeline" do a = :hey - expect ~M{a, b: a |> Atom.to_string} |> to(eq(%{a: :hey, b: "hey"})) + expect(~M{a, b: a |> Atom.to_string} |> to(eq(%{a: :hey, b: "hey"}))) end + example "string keys" do a = "blah" b = "bleh" - expect ~m{a, "b" => ~m(a, b)} |> to(eq(%{"a" => "blah", "b" => %{"a" => "blah", "b" => "bleh"}})) + + expect( + ~m{a, "b" => ~m(a, b)} + |> to(eq(%{"a" => "blah", "b" => %{"a" => "blah", "b" => "bleh"}})) + ) end example "string interpolation" do a = "blah" b = "bleh" - expect ~M(a, b: "#{b <> b}, c") |> to(eq(%{a: a, b: "blehbleh, c"})) + expect(~M(a, b: "#{b <> b}, c") |> to(eq(%{a: a, b: "blehbleh, c"}))) end - - end describe "regressions and bugfixes" do example "of mixed-mode parse error" do a = 5 - expect ~M{key: [1, a, 2]} |> to(eq(%{key: [1, 5, 2]})) + expect(~M{key: [1, a, 2]} |> to(eq(%{key: [1, 5, 2]}))) end example "of import shadowing" do defmodule Test do - import ShorterMaps + import TinyMaps + def test do get_struct(:a) get_old_map(:a) @@ -345,29 +393,27 @@ defmodule ShorterMapsSpec do modifier(:a, :b) do_sigil_m(:a, :b) end + def get_struct(a), do: ~M{a} def get_old_map(a), do: a def expand_variables(a, b), do: {a, b} def expand_variable(a, b), do: {a, b} - def identify_entries(a,b,c), do: {a, b, c} + def identify_entries(a, b, c), do: {a, b, c} def check_entry(a, b), do: {a, b} def fix_key(a), do: a def modifier(a, b), do: {a, b} def do_sigil_m(a, b), do: {a, b} end - end example "of varname variations" do a? = 1 - expect ~M{a?} |> to(eq %{a?: 1}) + expect(~M{a?} |> to(eq(%{a?: 1}))) a5 = 2 - expect ~M{a5} |> to(eq %{a5: 2}) + expect(~M{a5} |> to(eq(%{a5: 2}))) a! = 3 - expect ~M{a!} |> to(eq %{a!: 3}) + expect(~M{a!} |> to(eq(%{a!: 3}))) end end - end - end diff --git a/test/shorter_maps_test.exs b/test/tiny_maps_test.exs similarity index 98% rename from test/shorter_maps_test.exs rename to test/tiny_maps_test.exs index 9ff408a..2708138 100644 --- a/test/shorter_maps_test.exs +++ b/test/tiny_maps_test.exs @@ -1,7 +1,7 @@ -defmodule ShorterMapsTest do +defmodule TinyMapsTest do alias ExUnit.TestModule use ExUnit.Case - import ShorterMaps + import TinyMaps def eval(quoted_code), do: fn -> Code.eval_quoted(quoted_code) end @@ -319,7 +319,7 @@ defmodule ShorterMapsTest do %{a: [], len: 0} = ~M{a, len: length(a)} end - test "embedded shortermap" do + test "embedded tinymap" do a = 1 b = 2 assert %{a: ^a, b: %{b: ^b}} = ~M{a, b: ~M(b)} @@ -362,7 +362,7 @@ defmodule ShorterMapsTest do test "of import shadowing" do defmodule Test do - import ShorterMaps + import TinyMaps def test do get_struct(:a)