From 4a12526992c561a20edaeb1f7bd9f300743962b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Thallis?= Date: Mon, 3 Aug 2026 22:33:48 -0300 Subject: [PATCH 1/2] fix: parse identifiers with leading underscores WhatsApp webhook payloads carry vendor keys such as _vnd, so journey expressions like @event.message._vnd.v1.chat must resolve. The atom grammar rejected underscores in the first position, truncating the expression at the first underscore-leading attribute and leaving the rest as literal text. A bare _ still does not parse as an identifier, so @(_) remains literal text as before. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 10 ++++++++++ lib/expression/parser.ex | 7 ++++++- test/expression/parser_test.exs | 30 ++++++++++++++++++++++++++++++ test/expression_test.exs | 16 ++++++++++++++++ 4 files changed, 62 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ccbdb9c..b05ae9db 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/lib/expression/parser.ex b/lib/expression/parser.ex index 823555f5..3897e66c 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/test/expression/parser_test.exs b/test/expression/parser_test.exs index bd735ff7..3274fe04 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 ecad3469..f7ae9ad1 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]", %{ From 3a53ee112f59bd0b0f42475964e0a99b2fc7c168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Thallis?= Date: Mon, 3 Aug 2026 22:33:48 -0300 Subject: [PATCH 2/2] chore: upgrade version Co-Authored-By: Claude Fable 5 --- README.md | 2 +- mix.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d5b06141..e55980f7 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/mix.exs b/mix.exs index 0045fe19..11a20c30 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 [