commit 4632519b994a3acf2d770a3f6a6c8e474fd313d2 Author: Andrea Date: Thu Oct 17 10:12:42 2024 +0200 init diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..34d1bd9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +makeup_prisma-*.tar + +# Temporary files, for example, from tests. +/tmp/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..674670d --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# MakeupPrisma + +A [Makeup](https://github.com/elixir-makeup/makeup/) lexer for Prisma. + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `makeup_prisma` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:makeup_prisma, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at . + +MakeupPrisma is derived from [MakeupGraphql](https://github.com/Billzabob/makeup_graphql). \ No newline at end of file diff --git a/lib/makeup_prisma.ex b/lib/makeup_prisma.ex new file mode 100644 index 0000000..b1d062a --- /dev/null +++ b/lib/makeup_prisma.ex @@ -0,0 +1,226 @@ +defmodule Makeup.Lexers.MakeupPrisma do + @moduledoc """ + A `makeup` lexer for Prisma. + """ + + import Makeup.Lexer.Combinators + import Makeup.Lexer.Groups + import NimbleParsec + + @behaviour Makeup.Lexer + + @impl Makeup.Lexer + def lex(text, opts \\ []) do + group_prefix = Keyword.get(opts, :group_prefix, random_prefix(10)) + {:ok, tokens, "", _, _, _} = root(text) + + tokens + |> postprocess([]) + |> match_groups(group_prefix) + end + + @impl Makeup.Lexer + def postprocess(tokens, _opts \\ []), do: tokens + + @impl Makeup.Lexer + defgroupmatcher(:match_groups, + parentheses: [ + open: [[{:punctuation, %{language: :prisma}, "("}]], + close: [[{:punctuation, %{language: :prisma}, ")"}]] + ], + list: [ + open: [ + [{:punctuation, %{language: :prisma}, "["}] + ], + close: [ + [{:punctuation, %{language: :prisma}, "]"}] + ] + ], + curly: [ + open: [ + [{:punctuation, %{language: :prisma}, "{"}] + ], + close: [ + [{:punctuation, %{language: :prisma}, "}"}] + ] + ] + ) + + # Codepoints + @horizontal_tab 0x0009 + @newline 0x000A + @carriage_return 0x000D + @space 0x0020 + @unicode_bom 0xFEFF + + any_unicode = utf8_char([]) + + unicode_bom = ignore(utf8_char([@unicode_bom])) + + whitespace = + ascii_string( + [ + @horizontal_tab, + @space + ], + min: 1 + ) + |> token(:whitespace) + + line_terminator = + choice([ + ascii_char([@newline]), + ascii_char([@carriage_return]) + |> optional(ascii_char([@newline])) + ]) + |> token(:whitespace) + + comment = + string("//") + |> repeat_while(any_unicode, {:not_line_terminator, []}) + |> token(:comment_single) + + comma = ascii_char([?,]) |> token(:punctuation) + + punctuator = + ascii_char([ + ?(, + ?), + ?:, + ?=, + ?@, + ?[, + ?], + ?{, + ?|, + ?} + ]) + |> token(:punctuation) + + boolean_value_or_name_or_reserved_word = + ascii_char([?_, ?A..?Z, ?a..?z]) + |> repeat(ascii_char([?_, ?0..?9, ?A..?Z, ?a..?z])) + |> optional(ascii_char([??])) + |> post_traverse({:boolean_value_or_name_or_reserved_word, []}) + + negative_sign = ascii_char([?-]) + + digit = ascii_char([?0..?9]) + + non_zero_digit = ascii_char([?1..?9]) + + integer_part = + optional(negative_sign) + |> choice([ + ascii_char([?0]), + non_zero_digit |> repeat(digit) + ]) + + int_value = + empty() + |> concat(integer_part) + |> token(:number_integer) + + fractional_part = + ascii_char([?.]) + |> times(digit, min: 1) + + float_value = + choice([ + integer_part |> concat(fractional_part), + integer_part |> post_traverse({:fill_mantissa, []}), + integer_part |> concat(fractional_part) + ]) + |> token(:number_float) + + unicode_char_in_string = + string("\\u") + |> ascii_string([?0..?9, ?a..?f, ?A..?F], 4) + |> token(:string_escape) + + escaped_char = + string("\\") + |> utf8_string([], 1) + |> token(:string_escape) + + combinators_inside_string = [ + unicode_char_in_string, + escaped_char + ] + + string_value = string_like("\"", "\"", combinators_inside_string, :string) + + block_string_value = string_like(~S["""], ~S["""], combinators_inside_string, :string) + + root_element_combinator = + choice([ + unicode_bom, + whitespace, + line_terminator, + comment, + comma, + punctuator, + block_string_value, + string_value, + float_value, + int_value, + boolean_value_or_name_or_reserved_word + ]) + + @doc false + def __as_prisma_language__({ttype, meta, value}) do + {ttype, Map.put(meta, :language, :prisma), value} + end + + defparsec( + :root_element, + root_element_combinator |> map({__MODULE__, :__as_prisma_language__, []}) + ) + + defparsec( + :root, + repeat(parsec(:root_element)) + ) + + defp fill_mantissa(_rest, raw, context, _, _), do: {~c"0." ++ raw, context} + + @boolean_words ~w( + true + false + ) |> Enum.map(&String.to_charlist/1) + + @reserved_words ~w( + enum + model + datasource + generator + ) |> Enum.map(&String.to_charlist/1) + + defp boolean_value_or_name_or_reserved_word(rest, chars, context, loc, byte_offset) do + value = chars |> Enum.reverse() + do_boolean_value_or_name_or_reserved_word(rest, value, context, loc, byte_offset) + end + + defp do_boolean_value_or_name_or_reserved_word(_rest, value, context, _loc, _byte_offset) + when value in @boolean_words do + {[{:name_constant, %{}, value}], context} + end + + defp do_boolean_value_or_name_or_reserved_word(_rest, value, context, _loc, _byte_offset) + when value in @reserved_words do + {[{:keyword_reserved, %{}, value}], context} + end + + defp do_boolean_value_or_name_or_reserved_word(_rest, value, context, _loc, _byte_offset) do + {[{:name, %{}, value}], context} + end + + def line_and_column({line, line_offset}, byte_offset, column_correction) do + column = byte_offset - line_offset - column_correction + 1 + {line, column} + end + + defp not_line_terminator(<>, context, _, _), do: {:halt, context} + defp not_line_terminator(<>, context, _, _), do: {:halt, context} + defp not_line_terminator(_, context, _, _), do: {:cont, context} +end diff --git a/lib/makeup_prisma/application.ex b/lib/makeup_prisma/application.ex new file mode 100644 index 0000000..655f5d0 --- /dev/null +++ b/lib/makeup_prisma/application.ex @@ -0,0 +1,17 @@ +defmodule MakeupPrisma.Application do + @moduledoc false + use Application + + alias Makeup.Registry + alias Makeup.Lexers.MakeupPrisma + + def start(_type, _args) do + Registry.register_lexer(MakeupPrisma, + options: [], + names: ["prisma"], + extensions: ["prisma"] + ) + + Supervisor.start_link([], strategy: :one_for_one) + end +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..8f911fd --- /dev/null +++ b/mix.exs @@ -0,0 +1,38 @@ +defmodule MakeupPrisma.MixProject do + use Mix.Project + + @url "https://github.com/nullndr/makeup_prisma" + + def project do + [ + app: :makeup_prisma, + version: "0.1.0", + elixir: "~> 1.17", + start_permanent: Mix.env() == :prod, + deps: deps(), + package: package() + ] + end + + defp package do + [ + name: "makeup_prisma", + links: %{"GitHub" => @url} + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + {:makeup, "~> 1.0"}, + {:nimble_parsec, "~> 1.1"} + ] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..e097210 --- /dev/null +++ b/mix.lock @@ -0,0 +1,4 @@ +%{ + "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"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, +} diff --git a/test/makeup_prisma_test.exs b/test/makeup_prisma_test.exs new file mode 100644 index 0000000..b265620 --- /dev/null +++ b/test/makeup_prisma_test.exs @@ -0,0 +1,237 @@ +defmodule MakeupPrismaTest do + use ExUnit.Case + doctest MakeupPrisma + alias Makeup.Lexer.Postprocess + + defp lex(text) do + text + |> MakeupPrisma.lex(group_prefix: "group") + |> Postprocess.token_values_to_binaries() + |> Enum.map(fn {ttype, meta, value} -> {ttype, Map.delete(meta, :language), value} end) + end + + test "session from Shopify" do + schema = """ + model Session { + id String @id + shop String + state String + isOnline Boolean @default(false) + scope String? + expires DateTime? + accessToken String + userId BigInt? + firstName String? + lastName String? + email String? + accountOwner Boolean @default(false) + locale String? + collaborator Boolean? @default(false) + emailVerified Boolean? @default(false) + } + """ + + assert lex(schema) == [ + {:keyword_reserved, %{}, "model"}, + {:whitespace, %{}, " "}, + {:name, %{}, "Session"}, + {:whitespace, %{}, " "}, + {:punctuation, %{}, "{"}, + {:whitespace, %{}, "\n"}, + {:whitespace, %{}, " "}, + {:name, %{}, "id"}, + {:whitespace, %{}, " "}, + {:name, %{}, "String"}, + {:whitespace, %{}, " "}, + {:punctuation, %{}, "@"}, + {:name, %{}, "id"}, + {:whitespace, %{}, "\n"}, + {:whitespace, %{}, " "}, + {:name, %{}, "shop"}, + {:whitespace, %{}, " "}, + {:name, %{}, "String"}, + {:whitespace, %{}, "\n"}, + {:whitespace, %{}, " "}, + {:name, %{}, "state"}, + {:whitespace, %{}, " "}, + {:name, %{}, "String"}, + {:whitespace, %{}, "\n"}, + {:whitespace, %{}, " "}, + {:name, %{}, "isOnline"}, + {:whitespace, %{}, " "}, + {:name, %{}, "Boolean"}, + {:whitespace, %{}, " "}, + {:punctuation, %{}, "@"}, + {:name, %{}, "default"}, + {:punctuation, %{}, "("}, + {:name_constant, %{}, "false"}, + {:punctuation, %{}, ")"}, + {:whitespace, %{}, "\n"}, + {:whitespace, %{}, " "}, + {:name, %{}, "scope"}, + {:whitespace, %{}, " "}, + {:name, %{}, "String?"}, + {:whitespace, %{}, "\n"}, + {:whitespace, %{}, " "}, + {:name, %{}, "expires"}, + {:whitespace, %{}, " "}, + {:name, %{}, "DateTime?"}, + {:whitespace, %{}, "\n"}, + {:whitespace, %{}, " "}, + {:name, %{}, "accessToken"}, + {:whitespace, %{}, " "}, + {:name, %{}, "String"}, + {:whitespace, %{}, "\n"}, + {:whitespace, %{}, " "}, + {:name, %{}, "userId"}, + {:whitespace, %{}, " "}, + {:name, %{}, "BigInt?"}, + {:whitespace, %{}, "\n"}, + {:whitespace, %{}, " "}, + {:name, %{}, "firstName"}, + {:whitespace, %{}, " "}, + {:name, %{}, "String?"}, + {:whitespace, %{}, "\n"}, + {:whitespace, %{}, " "}, + {:name, %{}, "lastName"}, + {:whitespace, %{}, " "}, + {:name, %{}, "String?"}, + {:whitespace, %{}, "\n"}, + {:whitespace, %{}, " "}, + {:name, %{}, "email"}, + {:whitespace, %{}, " "}, + {:name, %{}, "String?"}, + {:whitespace, %{}, "\n"}, + {:whitespace, %{}, " "}, + {:name, %{}, "accountOwner"}, + {:whitespace, %{}, " "}, + {:name, %{}, "Boolean"}, + {:whitespace, %{}, " "}, + {:punctuation, %{}, "@"}, + {:name, %{}, "default"}, + {:punctuation, %{}, "("}, + {:name_constant, %{}, "false"}, + {:punctuation, %{}, ")"}, + {:whitespace, %{}, "\n"}, + {:whitespace, %{}, " "}, + {:name, %{}, "locale"}, + {:whitespace, %{}, " "}, + {:name, %{}, "String?"}, + {:whitespace, %{}, "\n"}, + {:whitespace, %{}, " "}, + {:name, %{}, "collaborator"}, + {:whitespace, %{}, " "}, + {:name, %{}, "Boolean?"}, + {:whitespace, %{}, " "}, + {:punctuation, %{}, "@"}, + {:name, %{}, "default"}, + {:punctuation, %{}, "("}, + {:name_constant, %{}, "false"}, + {:punctuation, %{}, ")"}, + {:whitespace, %{}, "\n"}, + {:whitespace, %{}, " "}, + {:name, %{}, "emailVerified"}, + {:whitespace, %{}, " "}, + {:name, %{}, "Boolean?"}, + {:whitespace, %{}, " "}, + {:punctuation, %{}, "@"}, + {:name, %{}, "default"}, + {:punctuation, %{}, "("}, + {:name_constant, %{}, "false"}, + {:punctuation, %{}, ")"}, + {:whitespace, %{}, "\n"}, + {:punctuation, %{}, "}"}, + {:whitespace, %{}, "\n"} + ] + end + + test "generators and datasource" do + schema = """ + generator client { + provider = "prisma-client-js" + previewFeatures = ["fullTextSearch", "omitApi"] + } + + generator json { + provider = "prisma-json-types-generator" + } + + datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + } + """ + + assert lex(schema) == [ + {:keyword_reserved, %{}, "generator"}, + {:whitespace, %{}, " "}, + {:name, %{}, "client"}, + {:whitespace, %{}, " "}, + {:punctuation, %{}, "{"}, + {:whitespace, %{}, "\n"}, + {:whitespace, %{}, " "}, + {:name, %{}, "provider"}, + {:whitespace, %{}, " "}, + {:punctuation, %{}, "="}, + {:whitespace, %{}, " "}, + {:string, %{}, "\"prisma-client-js\""}, + {:whitespace, %{}, "\n"}, + {:whitespace, %{}, " "}, + {:name, %{}, "previewFeatures"}, + {:whitespace, %{}, " "}, + {:punctuation, %{}, "="}, + {:whitespace, %{}, " "}, + {:punctuation, %{}, "["}, + {:string, %{}, "\"fullTextSearch\""}, + {:punctuation, %{}, ","}, + {:whitespace, %{}, " "}, + {:string, %{}, "\"omitApi\""}, + {:punctuation, %{}, "]"}, + {:whitespace, %{}, "\n"}, + {:punctuation, %{}, "}"}, + {:whitespace, %{}, "\n"}, + {:whitespace, %{}, "\n"}, + {:keyword_reserved, %{}, "generator"}, + {:whitespace, %{}, " "}, + {:name, %{}, "json"}, + {:whitespace, %{}, " "}, + {:punctuation, %{}, "{"}, + {:whitespace, %{}, "\n"}, + {:whitespace, %{}, " "}, + {:name, %{}, "provider"}, + {:whitespace, %{}, " "}, + {:punctuation, %{}, "="}, + {:whitespace, %{}, " "}, + {:string, %{}, "\"prisma-json-types-generator\""}, + {:whitespace, %{}, "\n"}, + {:punctuation, %{}, "}"}, + {:whitespace, %{}, "\n"}, + {:whitespace, %{}, "\n"}, + {:keyword_reserved, %{}, "datasource"}, + {:whitespace, %{}, " "}, + {:name, %{}, "db"}, + {:whitespace, %{}, " "}, + {:punctuation, %{}, "{"}, + {:whitespace, %{}, "\n"}, + {:whitespace, %{}, " "}, + {:name, %{}, "provider"}, + {:whitespace, %{}, " "}, + {:punctuation, %{}, "="}, + {:whitespace, %{}, " "}, + {:string, %{}, "\"postgresql\""}, + {:whitespace, %{}, "\n"}, + {:whitespace, %{}, " "}, + {:name, %{}, "url"}, + {:whitespace, %{}, " "}, + {:punctuation, %{}, "="}, + {:whitespace, %{}, " "}, + {:name, %{}, "env"}, + {:punctuation, %{}, "("}, + {:string, %{}, "\"DATABASE_URL\""}, + {:punctuation, %{}, ")"}, + {:whitespace, %{}, "\n"}, + {:punctuation, %{}, "}"}, + {:whitespace, %{}, "\n"} + ] + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()