From e30ba5ef3fed7b1bcd3e248329ae60e0099858d0 Mon Sep 17 00:00:00 2001 From: Claudio Ortolina Date: Thu, 10 Oct 2024 08:33:47 +0100 Subject: [PATCH] Implement basic parser --- lib/music_library/records/search_parser.ex | 62 +++++++++++++++++++ mix.exs | 1 + mix.lock | 1 + .../records/search_parser_test.exs | 5 ++ 4 files changed, 69 insertions(+) create mode 100644 lib/music_library/records/search_parser.ex create mode 100644 test/music_library/records/search_parser_test.exs diff --git a/lib/music_library/records/search_parser.ex b/lib/music_library/records/search_parser.ex new file mode 100644 index 00000000..ebe497ae --- /dev/null +++ b/lib/music_library/records/search_parser.ex @@ -0,0 +1,62 @@ +defmodule MusicLibrary.Records.SearchParser do + import NimbleParsec + + word = utf8_string([not: ?\s], min: 1) + space = ignore(string(" ")) + + quoted_words = + ignore(string(~s("))) + |> utf8_string([not: ?\"], min: 1) + |> ignore(string(~s("))) + + query = choice([quoted_words, word]) |> tag(:query) + + artist_filter = ignore(string("artist:")) + artist = concat(artist_filter, query) |> tag(:artist) + + album_filter = ignore(string("album:")) + album = concat(album_filter, query) |> tag(:album) + + mbid_filter = ignore(string("mbid:")) + mbid = concat(mbid_filter, query) |> tag(:mbid) + + search = repeat(choice([artist, album, mbid, space, query])) + + defparsecp(:search_parser, search) + + @doc """ + Parse a search query. + + iex> MusicLibrary.Records.SearchParser.parse("") + {:ok, %{}} + iex> MusicLibrary.Records.SearchParser.parse("marbles") + {:ok, %{query: "marbles"}} + iex> MusicLibrary.Records.SearchParser.parse("artist:marillion album:marbles") + {:ok, %{artist: "marillion", album: "marbles"}} + iex> MusicLibrary.Records.SearchParser.parse("artist:marillion album:fugazi artist:fish") + {:ok, %{artist: "marillion fish", album: "fugazi"}} + """ + def parse(query) do + {:ok, result, _rest, _context, _line, _byte_offset} = search_parser(query) + {:ok, normalize(result)} + end + + defp normalize(result) do + Enum.reduce(result, %{}, fn + {:artist, [{:query, [value]}]}, acc -> + Map.update(acc, :artist, value, &(&1 <> " " <> value)) + + {:album, [{:query, [value]}]}, acc -> + Map.update(acc, :album, value, &(&1 <> " " <> value)) + + {:mbid, [{:query, [value]}]}, acc -> + Map.put(acc, :mbid, value) + + {:query, [value]}, acc -> + Map.update(acc, :query, value, &(&1 <> " " <> value)) + + _, acc -> + acc + end) + end +end diff --git a/mix.exs b/mix.exs index aaab16f9..74eeeb0e 100644 --- a/mix.exs +++ b/mix.exs @@ -41,6 +41,7 @@ defmodule MusicLibrary.MixProject do {:yaml_elixir, "~> 2.11"}, {:finch, "~> 0.19.0"}, {:vix, "~> 0.30.0"}, + {:nimble_parsec, "~> 1.4"}, # TODO bump on release to {:phoenix_live_view, "~> 1.0.0"}, {:phoenix_live_view, "~> 1.0.0-rc.1", override: true}, {:floki, ">= 0.30.0", only: :test}, diff --git a/mix.lock b/mix.lock index 43f7f2f7..1825ddb4 100644 --- a/mix.lock +++ b/mix.lock @@ -29,6 +29,7 @@ "mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_ownership": {:hex, :nimble_ownership, "1.0.0", "3f87744d42c21b2042a0aa1d48c83c77e6dd9dd357e425a038dd4b49ba8b79a1", [:mix], [], "hexpm", "7c16cc74f4e952464220a73055b557a273e8b1b7ace8489ec9d86e9ad56cb2cc"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, diff --git a/test/music_library/records/search_parser_test.exs b/test/music_library/records/search_parser_test.exs new file mode 100644 index 00000000..0ca09dc4 --- /dev/null +++ b/test/music_library/records/search_parser_test.exs @@ -0,0 +1,5 @@ +defmodule MusicLibrary.Records.SearchParserTest do + use ExUnit.Case, async: true + + doctest MusicLibrary.Records.SearchParser +end