Skip to content

Commit

Permalink
Elixir 1.17 and 1.18 preparations
Browse files Browse the repository at this point in the history
  • Loading branch information
hissssst committed Jul 29, 2024
1 parent c8d4938 commit 2d384f0
Show file tree
Hide file tree
Showing 11 changed files with 130 additions and 85 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

Speed or composability? Choose both!

## Elixir 1.17 warning

It turns out that Elixir 1.17 release has 3 bugs which block reliable usage of Pathex library, one of which is critical.
So please, do not use Elixir 1.17 with the Pathex library. However, these issues are fixed in upcoming Elixir 1.18 release.

### What is Pathex?

Pathex is a library for performing fast actions with nested data structures in Elixir.
Expand Down
6 changes: 3 additions & 3 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{
description = "Pathex: Elixir language lenses and Access replacement";
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
outputs = { nixpkgs, ... }:
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
in {
outputs =
{ nixpkgs, ... }:
let pkgs = nixpkgs.legacyPackages.x86_64-linux; in
{
devShells.x86_64-linux.default = import ./shell.nix { inherit pkgs; };
};
}
47 changes: 16 additions & 31 deletions lib/pathex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -205,35 +205,35 @@ defmodule Pathex do
@doc """
Sets the `value` under `path` in `struct`.
### Creates path if it is not present
If the path does not exist it creates the path favouring maps
when structure is unknown.
## Example
iex> x = 1
iex> {:ok, [0, %{x: 123}]} = force_set [0, %{x: 8}], path(x / :x), 123
iex> p = path "hey" / 0
iex> {:ok, %{"hey" => %{0 => 1}}} = force_set %{}, p, 1
If the item in path doesn't have the right type, it returns `:error`.
### Incorrect types may be detected during call
## Example
If the item in path doesn't have the right type, it returns `:error`.
iex> p = path "hey" / "you"
iex> :error = force_set %{"hey" => {1, 2}}, p, "value"
### Empty space is filled with nil
Note that for paths created with `Pathex.path/2` list and tuple indexes
which are out of bounds fill the empty space with `nil`.
## Example
iex> p = path 4
iex> {:ok, [1, 2, 3, nil, 5]} = force_set [1, 2, 3], p, 5
iex> {:ok, {1, 2, 3, nil, 5}} = force_set {1, 2, 3}, p, 5
This is also true for negative indexes (except -1 for lists which always prepends)
### Negative indexes
## Example
This is also true for negative indexes (except -1 for lists which always prepends)
iex> p = path -5
iex> {:ok, [0, nil, 1, 2, 3]} = force_set [1, 2, 3], p, 0
Expand All @@ -252,36 +252,36 @@ defmodule Pathex do
@doc """
Sets the `value` under `path` in `struct`.
### Creates path if it is not present
If the path does not exist it creates the path favouring maps
when structure is unknown.
## Example
iex> x = 1
iex> [0, %{x: 123}] = force_set! [0, %{x: 8}], path(x / :x), 123
iex> p = path "hey" / 0
iex> %{"hey" => %{0 => 1}} = force_set! %{}, p, 1
If the item in path doesn't have the right type, it raises.
### Incorrect types may be detected during call
## Example
If the item in path doesn't have the right type, it raises.
iex> p = path "hey" / "you"
iex> force_set! %{"hey" => {1, 2}}, p, "value"
** (Pathex.Error) Type mismatch in structure
### Empty space is filled with nil
Note that for paths created with `Pathex.path/2` list and tuple indexes
which are out of bounds fill the empty space with `nil`.
## Example
iex> p = path 4
iex> [1, 2, 3, nil, 5] = force_set! [1, 2, 3], p, 5
iex> {1, 2, 3, nil, 5} = force_set! {1, 2, 3}, p, 5
This is also true for negative indexes (except -1 for lists which always prepends)
### Negative indexes
## Example
This is also true for negative indexes (except `-1` for lists which always prepends)
iex> p = path -5
iex> [0, nil, 1, 2, 3] = force_set! [1, 2, 3], p, 0
Expand Down Expand Up @@ -794,26 +794,11 @@ defmodule Pathex do

# Case for not inlined paths
:error ->
# case Macro.expand(path, caller) do
# {:fn, _, clauses} ->
# Enum.find_value(clauses, fn
# {:"->", _, [[^op, args], body]} -> {args, body}
# _ -> false
# end)
# |> IO.inspect()

# IO.puts "YEAH!"

# _ ->
# :error
# end

quote generated: true do
unquote(code).(unquote(op), {unquote_splicing(args)})
end
|> Common.set_generated()
end

# |> tap(fn x -> IO.puts Macro.to_string x end)
end

Expand Down
11 changes: 10 additions & 1 deletion lib/pathex/builder/updaters/force_updater.ex
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,20 @@ defmodule Pathex.Builder.ForceUpdater do
collection(type, [acc_items])
end

defp add_to_acc({type, n}, acc_items) when is_integer(n) do
defp add_to_acc({type, -1}, acc_items) do
collection(type, [acc_items])
end

defp add_to_acc({type, n}, acc_items) when is_integer(n) and n > 0 do
nils = for _ <- 1..n, do: nil
collection(type, nils ++ [acc_items])
end

defp add_to_acc({type, n}, acc_items) when is_integer(n) and n < 0 do
nils = for _ <- 1..(-n), do: nil
collection(type, [acc_items | nils])
end

defp add_to_acc({:list, var}, acc_items) do
quote do
case unquote(var) do
Expand Down
51 changes: 34 additions & 17 deletions lib/pathex/operations.ex
Original file line number Diff line number Diff line change
Expand Up @@ -83,25 +83,42 @@ defmodule Pathex.Operations do
raise ArgumentError, "Modificator #{mod} is not supported"
end

@spec filter_combination(Pathex.Combination.t(), Pathex.mod()) :: Pathex.Combination.t()
def filter_combination(combination, mod) do
combination
|> Enum.map(&filter_one(mod, &1))
|> Enum.filter(fn
[] -> false
_ -> true
end)
@spec filter_combination(Pathex.Combination.t(), Pathex.mod(), Pathex.Combination.t()) :: {:ok, Pathex.Combination.t()} | {:error, any()}
def filter_combination(combination, mod, acc \\ [])
def filter_combination([], _mod, acc), do: {:ok, Enum.reverse(acc)}
def filter_combination([head | tail], mod, acc) do
with {:ok, step} <- filter_one(mod, head) do
filter_combination(tail, mod, [step | acc])
end
end

defp filter_one(:naive, path), do: path
defp filter_one(:map, path), do: Keyword.take(path, [:map])

defp filter_one(:naive, [_ | _] = path), do: {:ok, path}
defp filter_one(:map, path) do
case Keyword.fetch(path, :map) do
:error -> {:error, "At least some map key expected, but got only #{inspect(path)}"}
{:ok, map_key} -> {:ok, map: map_key}
end
end
defp filter_one(:json, path) do
Enum.filter(path, fn
{:map, i} when is_integer(i) -> false
{:map, _} -> true
{:list, i} when is_integer(i) and i >= 0 -> true
_ -> false
end)
filtered =
Enum.filter(path, fn
{:map, i} when is_integer(i) -> false
{:map, _} -> true
{:list, i} when is_integer(i) and i >= 0 -> true
_ -> false
end)

case filtered do
[] ->
[{_, value} | _] = path
types = Enum.map_join(path, ", ", fn {type, _} -> type end)
reason = ":json modifier expects any non-integer map key or positive list index literal. " <>
"But got #{value} for #{types}"

{:error, reason}

filtered ->
{:ok, filtered}
end
end
end
14 changes: 11 additions & 3 deletions lib/pathex/quoted_parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,17 @@ defmodule Pathex.QuotedParser do
|> Enum.map(&detect_quoted/1)
|> Enum.unzip()

binds = Enum.reject(binds, &is_nil/1)
combination = Operations.filter_combination(combination, mod)
{binds, combination}
case Operations.filter_combination(combination, mod) do
{:ok, combination} ->
binds = Enum.reject(binds, &is_nil/1)
{binds, combination}

{:error, reason} ->
raise CompileError,
line: env.line,
file: env.file,
description: "The step is filtered by specified modifier. " <> reason
end
end

@doc """
Expand Down
3 changes: 1 addition & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ defmodule Pathex.MixProject do
[
app: :pathex,
version: @version,
elixir: "~> 1.13",
elixir: ">= 1.13.0 and < 1.17.0 or > 1.18.0 or == 1.18.0-dev",
start_permanent: Mix.env() == :prod,
description: description(),
package: package(),
Expand Down Expand Up @@ -51,7 +51,6 @@ defmodule Pathex.MixProject do
[
# # Uncomment for development
{:dialyxir, "~> 1.0", only: :dev, runtime: false},
{:gradient, github: "esl/gradient", only: :dev, runtime: false},
{:mix_unused, "~> 0.3", only: :dev, runtime: false},
{:credo, "~> 1.5", only: :dev, runtime: false},
{:ex_doc, "~> 0.28", only: :dev, runtime: false}
Expand Down
16 changes: 8 additions & 8 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
%{
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"credo": {:hex, :credo, "1.7.5", "643213503b1c766ec0496d828c90c424471ea54da77c8a168c725686377b9545", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f799e9b5cd1891577d8c773d245668aa74a2fcd15eb277f51a0131690ebfb3fd"},
"credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"},
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"ex_doc": {:hex, :ex_doc, "0.31.1", "8a2355ac42b1cc7b2379da9e40243f2670143721dd50748bf6c3b1184dae2089", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3178c3a407c557d8343479e1ff117a96fd31bafe52a039079593fb0524ef61b0"},
"earmark_parser": {:hex, :earmark_parser, "1.4.40", "f3534689f6b58f48aa3a9ac850d4f05832654fe257bf0549c08cc290035f70d5", [:mix], [], "hexpm", "cdb34f35892a45325bad21735fadb88033bcb7c4c296a999bde769783f53e46a"},
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
"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"},
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
"gradient": {:git, "https://github.com/esl/gradient.git", "33e13fbe1ff60a49abdc638d76b77effddf3cc45", []},
"gradient_macros": {:git, "https://github.com/esl/gradient_macros.git", "3bce2146bf0cdf380f773c40e2b7bd6558ab6de8", [ref: "3bce214"]},
"gradualizer": {:git, "https://github.com/josefs/Gradualizer.git", "3021d29d82741399d131e3be38d2a8db79d146d4", [tag: "0.3.0"]},
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
"jason": {:hex, :jason, "1.4.3", "d3f984eeb96fe53b85d20e0b049f03e57d075b5acda3ac8d465c969a2536c17b", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "9a90e868927f7c777689baa16d86f4d0e086d968db5c05d917ccff6d443e58a3"},
"libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
"makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [: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", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.3", "d684f4bac8690e70b06eb52dad65d26de2eefa44cd19d64a8095e1417df7c8fd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b78dc853d2e670ff6390b605d807263bf606da3c82be37f9d7f68635bd886fc9"},
"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"},
"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"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"},
"mix_unused": {:hex, :mix_unused, "0.4.1", "9f8d759a300a79d2077d6baf617f3a5af6935d50b0f113c09295b265afc3e411", [:mix], [{:libgraph, ">= 0.0.0", [hex: :libgraph, repo: "hexpm", optional: false]}], "hexpm", "fa21f688a88e0710e3d96ac1c8e5a6181aea8a75c8a4214f0edcfeb069b831a3"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
}
21 changes: 15 additions & 6 deletions shell.nix
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
{ pkgs ? (import <nixpkgs> { }), ... }:
with pkgs;
let otp = beam.packages.erlangR26;
in pkgs.mkShell {
let
otp = beam.packages.erlang_27;
elixir = otp.elixir_1_17.overrideAttrs (oldAttrs: {
version = "1.18-dev";
src = fetchFromGitHub {
owner = "elixir-lang";
repo = "elixir";
rev = "b799e9eda4613a1bc40fd0824fb08d5df3b3e24b";
sha256 = "sha256-mTG9/Qk/kXoVdWL9oBv1WuNMyQjIINx8Gpi91l6oS4M=";
};
});
in
pkgs.mkShell {
buildInputs = [
otp.elixir_1_16
elixir
otp.erlang
((if otp ? elixir-ls then otp.elixir-ls else otp.elixir_ls).override {
elixir = otp.elixir_1_16;
})
((if otp ? elixir-ls then otp.elixir-ls else otp.elixir_ls).override { inherit elixir; })
];

shellHook = ''
Expand Down
34 changes: 23 additions & 11 deletions test/pathex_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,18 @@ defmodule PathexTest do
assert :error = view([{"hey", [1, "yay!"]}], path)
end

test "view: json path: long hard" do
path = path("hey" / 1 / 1 / 1 / "x" / "y" / "z", :json)

assert {:ok, "yay!"} =
view(
%{
"hey" => [0, [0, [0, %{"x" => %{"y" => %{"z" => "yay!"}}}]]]
},
path
)
end
# @tag skip: true
# test "view: json path: long hard" do
# path = path("hey" / 1 / 1 / 1 / "x" / "y" / "z", :json)

# assert {:ok, "yay!"} =
# view(
# %{
# "hey" => [0, [0, [0, %{"x" => %{"y" => %{"z" => "yay!"}}}]]]
# },
# path
# )
# end

test "view: path: easy" do
path = path(1 / :x / 3)
Expand All @@ -100,6 +101,7 @@ defmodule PathexTest do
assert {:ok, 1} = view(%{variable => %{variable => 1}}, p)
assert :error = view(%{{:variable, [], Elixir} => %{x: 1}}, p)
assert :error = view(%{{:variable, [], nil} => %{x: 1}}, p)

variable = :y
assert {:ok, 1} = view(%{x: %{x: 1}}, p)
assert :error = view(%{x: %{y: 1}}, p)
Expand Down Expand Up @@ -249,6 +251,15 @@ defmodule PathexTest do
assert [1, 2, 3, nil, 0] = force_set!([1, 2, 3], path(4), 0)
end

test "deep force_set: -1" do
assert %{x: [123]} = force_set!(%{}, path(:x / (-1 :: :list)), 123)
assert [x: [123]] = force_set!([], path(:x / (-1 :: :list)), 123)
assert [[123]] = force_set!([], path(-1 / (-1 :: :list)), 123)

assert %{x: [123]} = force_set!(%{x: []}, path(:x / (-1 :: :list)), 123)
assert %{x: [123]} = force_set!(%{x: []}, path(:x / (-1 :: :list)), 123)
end

test "inlined: path: view" do
assert {:ok, 1} == view(%{x: %{y: 1}}, path(:x / :y))
end
Expand Down Expand Up @@ -287,4 +298,5 @@ defmodule PathexTest do
assert true == exists?(s, path(0) ~> path(:x))
assert false == exists?(s, path(0) ~> path(:y))
end

end

0 comments on commit 2d384f0

Please sign in to comment.