From 7dcc893297f45923cfa9ee2bb3341971278cde45 Mon Sep 17 00:00:00 2001 From: Kieran Eglin Date: Sat, 20 Jan 2024 18:54:17 -0800 Subject: [PATCH] Added basic parser for hydrating template strings --- lib/pinchflat/rendered_string/base.ex | 52 +++++++++++++++++++ lib/pinchflat/rendered_string/parser.ex | 31 ++++++++++- .../backends}/yt_dlp/command_runner_test.exs | 0 .../yt_dlp/video_collection_test.exs | 0 .../pinchflat/rendered_string/parser_test.exs | 40 ++++++++++++++ 5 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 lib/pinchflat/rendered_string/base.ex rename test/pinchflat/{downloader_backends => downloader/backends}/yt_dlp/command_runner_test.exs (100%) rename test/pinchflat/{downloader_backends => downloader/backends}/yt_dlp/video_collection_test.exs (100%) create mode 100644 test/pinchflat/rendered_string/parser_test.exs diff --git a/lib/pinchflat/rendered_string/base.ex b/lib/pinchflat/rendered_string/base.ex new file mode 100644 index 0000000..ee0e05f --- /dev/null +++ b/lib/pinchflat/rendered_string/base.ex @@ -0,0 +1,52 @@ +defmodule Pinchflat.RenderedString.Base do + @moduledoc """ + A base module for parsing rendered strings, designed as a macro to be used + in other modules. + + NOTE: if the needs here get any more complicated, look into using a Liquid + template parser. No need to reinvent the wheel any more than I already have. + + NOTE: this is effectively tested by the `Pinchflat.RenderedString.Parser`'s tests + """ + + defmacro __using__(_opts) do + quote location: :keep do + import NimbleParsec + + opening_tag = string("{{") + closing_tag = string("}}") + optional_whitespaces = ascii_string(~c[ \t\n\r], min: 0) + + # Capture everything up to the opening object + text = + lookahead_not(opening_tag) + # ... as long as it's a character + |> utf8_char([]) + # ... and there's at least one character + |> times(min: 1) + # ... and then convert it to a string + |> reduce({List, :to_string, []}) + # ... finally bag it and tag it + |> unwrap_and_tag(:text) + + identifier = + utf8_string([?a..?z, ?A..?Z, ?_, ?0..?9], min: 1) + |> reduce({Enum, :join, []}) + |> unwrap_and_tag(:identifier) + + defparsecp(:expression, identifier) + + # when spotting interpolation, ignore the opening tag and any whitespace + interpolation = + ignore(concat(opening_tag, optional_whitespaces)) + # ... then parse the expression (identifier) + |> parsec(:expression) + # ... then ignore any whitespace and the closing tags after the expression + |> ignore(concat(optional_whitespaces, closing_tag)) + # ... once again we bag it and tag it + |> unwrap_and_tag(:interpolation) + + defparsec(:do_parse, choice([interpolation, text]) |> repeat() |> eos()) + end + end +end diff --git a/lib/pinchflat/rendered_string/parser.ex b/lib/pinchflat/rendered_string/parser.ex index 3b27286..b3aaed1 100644 --- a/lib/pinchflat/rendered_string/parser.ex +++ b/lib/pinchflat/rendered_string/parser.ex @@ -1,5 +1,34 @@ -defmodule Pinchflat.Downloader.RenderedString.Parser do +defmodule Pinchflat.RenderedString.Parser do @moduledoc """ Parses liquid-ish-style strings into a rendered string + + Used for turning filepath templates into real filepaths """ + + use Pinchflat.RenderedString.Base + + @doc """ + Parses a string into a rendered string, using the provided variables. + + The variable keys MUST be strings. + """ + def parse(string, variables) do + # `do_parse` comes from `RenderedString.Base` + case do_parse(string) do + {:ok, parsed, _, _, _, _} -> + {:ok, build_string(parsed, variables)} + + {:error, message, _, _, _, _} -> + {:error, message} + end + end + + defp build_string(parsed, variables) do + Enum.reduce(parsed, "", fn element, acc -> + case element do + {:text, text} -> acc <> text + {:interpolation, {:identifier, identifier}} -> acc <> to_string(variables[identifier]) + end + end) + end end diff --git a/test/pinchflat/downloader_backends/yt_dlp/command_runner_test.exs b/test/pinchflat/downloader/backends/yt_dlp/command_runner_test.exs similarity index 100% rename from test/pinchflat/downloader_backends/yt_dlp/command_runner_test.exs rename to test/pinchflat/downloader/backends/yt_dlp/command_runner_test.exs diff --git a/test/pinchflat/downloader_backends/yt_dlp/video_collection_test.exs b/test/pinchflat/downloader/backends/yt_dlp/video_collection_test.exs similarity index 100% rename from test/pinchflat/downloader_backends/yt_dlp/video_collection_test.exs rename to test/pinchflat/downloader/backends/yt_dlp/video_collection_test.exs diff --git a/test/pinchflat/rendered_string/parser_test.exs b/test/pinchflat/rendered_string/parser_test.exs new file mode 100644 index 0000000..b7c4b70 --- /dev/null +++ b/test/pinchflat/rendered_string/parser_test.exs @@ -0,0 +1,40 @@ +defmodule Pinchflat.RenderedString.ParserTest do + use ExUnit.Case, async: true + + alias Pinchflat.RenderedString.Parser + + describe "parse/2" do + test "it returns the rendered string when the string is valid" do + assert {:ok, "bar"} = Parser.parse("{{ foo }}", %{"foo" => "bar"}) + end + + test "it works with filepath-like strings" do + assert {:ok, "bar/baz"} = + Parser.parse("{{ foo }}/{{ bar }}", %{"foo" => "bar", "bar" => "baz"}) + end + + test "it works when mixing text and variables" do + assert {:ok, "bar text baz"} = + Parser.parse("{{ foo }} text {{ bar }}", %{"foo" => "bar", "bar" => "baz"}) + end + + test "it removes the placeholder but doesn't blow up when the variable isn't provided" do + assert {:ok, ""} = Parser.parse("{{ foo }}", %{}) + end + + test "it accepts any number of spaces between open and closing tags" do + assert {:ok, "bar"} = Parser.parse("{{foo}}", %{"foo" => "bar"}) + assert {:ok, "bar"} = Parser.parse("{{ foo}}", %{"foo" => "bar"}) + assert {:ok, "bar"} = Parser.parse("{{foo }}", %{"foo" => "bar"}) + assert {:ok, "bar"} = Parser.parse("{{ foo }}", %{"foo" => "bar"}) + end + + test "it doesn't interpret single braces as variables" do + assert {:ok, "{foo}"} = Parser.parse("{foo}", %{}) + end + + test "it returns an error when the string is invalid" do + assert {:error, "expected end of string"} = Parser.parse("{{ 1-1 }", %{}) + end + end +end