diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ccbdb9..b05ae9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## v3.0.0-rc.2 + +### Fixed + +- Identifiers may now start with underscores, so attribute access on + WhatsApp webhook vendor keys such as `@event.message._vnd.v1.chat` + parses fully instead of stopping at the underscore. A bare `_` is + still not an identifier: `@(_)` remains literal text, and unresolved + variables like `@_missing` render back as-is. + ## v3.0.0-rc.0 This is the first release candidate for v3.0.0. It contains the breaking diff --git a/README.md b/README.md index d5b0614..e55980f 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ by adding `expression` to your list of dependencies in `mix.exs`: ```elixir def deps do [ - {:expression, "~> 3.0.0-rc.1"} + {:expression, "~> 3.0.0-rc.2"} ] end ``` diff --git a/lib/expression/parser.ex b/lib/expression/parser.ex index 823555f..3897e66 100644 --- a/lib/expression/parser.ex +++ b/lib/expression/parser.ex @@ -36,8 +36,13 @@ defmodule Expression.Parser do ) # atom = atom + # + # Leading underscores are valid (WhatsApp webhook payloads have keys such as + # `_vnd`) but an atom needs at least one letter or digit, so that a bare `_` + # keeps failing to parse and `@(_)` remains literal text. atom = - ascii_string([?a..?z, ?A..?Z, ?0..?9], min: 1) + ascii_string([?_], min: 0) + |> ascii_string([?a..?z, ?A..?Z, ?0..?9], min: 1) |> ascii_string([?a..?z, ?A..?Z, ?0..?9, ?_, ?-], min: 0) |> map({String, :downcase, []}) |> reduce({Enum, :join, []}) diff --git a/mix.exs b/mix.exs index 0045fe1..11a20c3 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Expression.MixProject do use Mix.Project - @version "3.0.0-rc.1" + @version "3.0.0-rc.2" def project do [ diff --git a/test/expression/parser_test.exs b/test/expression/parser_test.exs index bd735ff..3274fe0 100644 --- a/test/expression/parser_test.exs +++ b/test/expression/parser_test.exs @@ -10,6 +10,11 @@ defmodule Expression.ParserTest do assert_ast([expression: [atom: "foo"]], "@foo") end + test "expression with a leading underscore" do + assert_ast([expression: [atom: "_foo"]], "@_foo") + assert_ast([expression: [atom: "__foo"]], "@__foo") + end + test "escaped at" do assert_ast([text: "user", text: "@", text: "example.org"], "user@@example.org") end @@ -419,6 +424,31 @@ defmodule Expression.ParserTest do ) end + test "on keys with leading underscores" do + assert_ast( + [ + expression: [ + attribute: [ + attribute: [ + attribute: [ + attribute: [atom: "event", atom: "message"], + atom: "_vnd" + ], + atom: "v1" + ], + atom: "chat" + ] + ] + ], + "@event.message._vnd.v1.chat" + ) + + assert_ast( + [expression: [attribute: [atom: "foo", atom: "_bar"]]], + "@(foo._bar)" + ) + end + test "on functions" do assert_ast( [ diff --git a/test/expression_test.exs b/test/expression_test.exs index ecad346..f7ae9ad 100644 --- a/test/expression_test.exs +++ b/test/expression_test.exs @@ -197,6 +197,22 @@ defmodule ExpressionTest do assert "bar" == Expression.evaluate_as_string!("@foo[1]", %{"foo" => ["baz", "bar"]}) end + test "attributes with leading underscores" do + context = %{ + "event" => %{ + "message" => %{"_vnd" => %{"v1" => %{"chat" => %{"state" => "OPEN"}}}} + } + } + + assert "OPEN" == + Expression.evaluate_as_string!("@event.message._vnd.v1.chat.state", context) + + assert %{"state" => "OPEN"} == + Expression.evaluate_block!("event.message._vnd.v1.chat", context) + + assert "@_missing" == Expression.evaluate_as_string!("@_missing", %{}) + end + test "list with variable" do assert "bar" = Expression.evaluate_as_string!("@foo[cursor]", %{