This commit is contained in:
Andrea 2024-10-17 10:12:42 +02:00
commit 4632519b99
No known key found for this signature in database
GPG Key ID: 4594610B9C8F91C5
9 changed files with 575 additions and 0 deletions

4
.formatter.exs Normal file
View File

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

26
.gitignore vendored Normal file
View File

@ -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/

22
README.md Normal file
View File

@ -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 <https://hexdocs.pm/makeup_prisma>.
MakeupPrisma is derived from [MakeupGraphql](https://github.com/Billzabob/makeup_graphql).

226
lib/makeup_prisma.ex Normal file
View File

@ -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(<<?\n, _::binary>>, context, _, _), do: {:halt, context}
defp not_line_terminator(<<?\r, _::binary>>, context, _, _), do: {:halt, context}
defp not_line_terminator(_, context, _, _), do: {:cont, context}
end

View File

@ -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

38
mix.exs Normal file
View File

@ -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

4
mix.lock Normal file
View File

@ -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"},
}

237
test/makeup_prisma_test.exs Normal file
View File

@ -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

1
test/test_helper.exs Normal file
View File

@ -0,0 +1 @@
ExUnit.start()