Skip to content

Commit

Permalink
Import builder protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
lucacorti committed Sep 22, 2024
1 parent 3b11666 commit 6a1b28d
Show file tree
Hide file tree
Showing 11 changed files with 647 additions and 23 deletions.
463 changes: 463 additions & 0 deletions lib/sassone/builder.ex

Large diffs are not rendered by default.

93 changes: 93 additions & 0 deletions lib/sassone/builder/description.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
defmodule Sassone.Builder.Description do
@moduledoc """
A struct representing the description of an XML resource element or attribute.
"""

@type type :: :element | :attribute

@type field_name :: atom()

@type t :: %__MODULE__{
field_name: field_name(),
deserialize: boolean(),
many: boolean(),
resource: module(),
serialize: boolean(),
type: type(),
recased_name: String.t()
}

@enforce_keys [:field_name, :deserialize, :serialize, :type, :recased_name]
defstruct field_name: nil,
deserialize: true,
many: false,
resource: nil,
serialize: true,
type: nil,
recased_name: nil

schema = [
case: [
doc: "Recase the resource field names automatically with the given strategy.",
type: {:in, [:pascal, :camel, :snake, :kebab]},
default: :pascal
],
debug: [doc: "Enable debug for parser generation.", type: :boolean, default: false],
fields: [
doc:
"Resource fields to map to XML. The order of elements will be preserved in the generated XML.",
type: :keyword_list,
keys: [
*: [
type: :keyword_list,
keys: [
deserialize: [
doc: "If false, the resource field won't be deserialized from XML.",
type: :boolean,
default: true
],
serialize: [
doc: "If false, the resource field won't be serialized to XML.",
type: :boolean,
default: true
],
many: [
doc:
"Specifies if the element can be repeated and should be serialized and deserialized as a list.",
type: :boolean,
default: false
],
name: [
doc:
"Custom resource field name for serialization and deserialization. If defined, it will be used as-is instead of recasing.",
type: :string
],
resource: [
doc:
"If the element is represented by another resource, it needs to be specified here.",
type: :atom
],
type: [
doc: "The XML shape that the resource field has in XML.",
type: {:in, [:element, :attribute]},
default: :element
]
]
]
],
required: true
],
namespace: [
doc: "XML namespace to apply to the resource when serializing.",
type: {:or, [:string, nil]},
default: nil
],
root_element: [
doc: "XML root element. This applies only to the toplevel Resource when (de)serializing.",
type: :string,
default: "Root"
]
]

def __schema__, do: unquote(schema)
end
4 changes: 4 additions & 0 deletions lib/sassone/builder/parser.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
defmodule Sassone.Builder.Parser do
@moduledoc false
defstruct parsers: [], elements: [], keys: [], state: %{}
end
24 changes: 12 additions & 12 deletions lib/sassone/parser.ex
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
defmodule Sassone.Parser do
@moduledoc false

alias Sassone.Parser.Generator

defmodule Binary do
@moduledoc false

use Sassone.Parser.Builder, streaming?: false
use Generator, streaming?: false
end

defmodule Stream do
@moduledoc false

use Sassone.Parser.Builder, streaming?: true
use Generator, streaming?: true
end

alias Sassone.Parser.State

@compile {:inline, [convert_entity_reference: 2]}

def convert_entity_reference(reference_name, %{expand_entity: :never}),
def convert_entity_reference(reference_name, :never),
do: [?&, reference_name, ?;]

def convert_entity_reference("amp", _state), do: [?&]
def convert_entity_reference("lt", _state), do: [?<]
def convert_entity_reference("gt", _state), do: [?>]
def convert_entity_reference("apos", _state), do: [?']
def convert_entity_reference("quot", _state), do: [?"]
def convert_entity_reference("amp", _expand_entity), do: [?&]
def convert_entity_reference("lt", _expand_entity), do: [?<]
def convert_entity_reference("gt", _expand_entity), do: [?>]
def convert_entity_reference("apos", _expand_entity), do: [?']
def convert_entity_reference("quot", _expand_entity), do: [?"]

def convert_entity_reference(reference_name, %State{} = state) do
case state.expand_entity do
def convert_entity_reference(reference_name, expand_entity) do
case expand_entity do
:keep -> [?&, reference_name, ?;]
:skip -> []
{mod, fun, args} -> apply(mod, fun, [reference_name | args])
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule Sassone.Parser.Builder do
defmodule Sassone.Parser.Generator do
@moduledoc false

import Sassone.Parser.Builder.{BufferingHelper, Guards, Lookahead}
import Sassone.Parser.Generator.{BufferingHelper, Guards, Lookahead}

defmacro __using__(options) do
quote location: :keep do
Expand Down Expand Up @@ -1074,7 +1074,7 @@ defmodule Sassone.Parser.Builder do
more?,
original,
pos,
state,
%State{} = state,
attributes,
open_quote,
att_name,
Expand Down Expand Up @@ -1130,7 +1130,7 @@ defmodule Sassone.Parser.Builder do

";" <> rest ->
name = binary_part(original, pos, len)
converted = Parser.convert_entity_reference(name, state)
converted = Parser.convert_entity_reference(name, state.expand_entity)
acc = [acc | converted]

att_value(
Expand Down Expand Up @@ -1542,7 +1542,7 @@ defmodule Sassone.Parser.Builder do
end
end

defp element_entity_ref(<<buffer::bits>>, more?, original, pos, state, acc, len) do
defp element_entity_ref(<<buffer::bits>>, more?, original, pos, %State{} = state, acc, len) do
lookahead buffer, @streaming do
char <> rest when is_ascii_name_char(char) ->
element_entity_ref(rest, more?, original, pos, state, acc, len + 1)
Expand All @@ -1560,7 +1560,7 @@ defmodule Sassone.Parser.Builder do

";" <> rest ->
name = binary_part(original, pos, len)
char = Parser.convert_entity_reference(name, state)
char = Parser.convert_entity_reference(name, state.expand_entity)
chardata(rest, more?, original, pos + len + 1, state, [acc | char], 0)

_ in [""] when more? ->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule Sassone.Parser.Builder.BufferingHelper do
defmodule Sassone.Parser.Generator.BufferingHelper do
@moduledoc false

@doc "Define a named function that matches a token and returns the parsing context."
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule Sassone.Parser.Builder.Guards do
defmodule Sassone.Parser.Generator.Guards do
@moduledoc false

defguard is_ascii(codepoint) when codepoint <= 0x7F
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule Sassone.Parser.Builder.Lookahead do
defmodule Sassone.Parser.Generator.Lookahead do
@moduledoc false

def edge_ngrams(word) do
Expand Down
62 changes: 61 additions & 1 deletion lib/sassone/xml.ex
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ defmodule Sassone.XML do
]
}

alias Sassone.Encoder
alias Sassone.{Builder, Encoder}
alias Sassone.Builder.Description

@doc "Builds attribute in simple form."
@spec attribute(namespace(), name(), value()) :: attribute()
Expand Down Expand Up @@ -89,4 +90,63 @@ defmodule Sassone.XML do
@spec processing_instruction(String.t(), String.t()) :: processing_instruction()
def processing_instruction(name, instruction),
do: {:processing_instruction, name, Encoder.encode(instruction)}

@doc "Builds a resource for encoding with `Sassone.encode!/2"
@spec build_resource(Builder.t(), name()) :: element()
def build_resource(resource, element_name) do
attributes =
Builder.attributes(resource)
|> Enum.reduce([], &build_resource_attributes(resource, &1, &2))
|> Enum.reverse()

elements =
Builder.elements(resource)
|> Enum.reduce([], &build_resource_elements(resource, &1, &2))
|> Enum.reverse()

element(Builder.namespace(resource), element_name, attributes, elements)
end

defp build_resource_attributes(_resource, %Description{serialize: false}, attributes),
do: attributes

defp build_resource_attributes(resource, %Description{} = description, attributes) do
build_resource_attribute(
description,
Map.get(resource, description.field_name),
attributes
)
end

defp build_resource_attribute(_description, nil, attributes), do: attributes

defp build_resource_attribute(description, value, attributes),
do: [attribute(nil, description.recased_name, value) | attributes]

defp build_resource_elements(_resource, %Description{serialize: false}, elements),
do: elements

defp build_resource_elements(resource, %Description{} = description, elements) do
build_resource_element(
description,
Map.get(resource, description.field_name),
elements
)
end

defp build_resource_element(_description, value, elements) when value in [nil, []],
do: elements

defp build_resource_element(%Description{} = description, values, elements)
when is_list(values) do
Enum.reduce(values, elements, &build_resource_element(description, &1, &2))
end

defp build_resource_element(%Description{} = description, value, elements) do
if Builder.impl_for(value) do
[build_resource(value, description.recased_name) | elements]
else
[element(nil, description.recased_name, [], [characters(value)]) | elements]
end
end
end
4 changes: 3 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ defmodule Sassone.MixProject do
{:credo, "~> 1.0", only: :dev, runtime: false},
{:dialyxir, "~> 1.0", only: :dev, runtime: false},
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
{:stream_data, "~> 1.0", only: [:dev, :test]}
{:stream_data, "~> 1.0", only: [:dev, :test]},
{:nimble_options, "~> 1.0"},
{:recase, "~> 0.8"}
]
end

Expand Down
2 changes: 2 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"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"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"recase": {:hex, :recase, "0.8.1", "ab98cd35857a86fa5ca99036f575241d71d77d9c2ab0c39aacf1c9b61f6f7d1d", [:mix], [], "hexpm", "9fd8d63e7e43bd9ea385b12364e305778b2bbd92537e95c4b2e26fc507d5e4c2"},
"stream_data": {:hex, :stream_data, "1.1.1", "fd515ca95619cca83ba08b20f5e814aaf1e5ebff114659dc9731f966c9226246", [:mix], [], "hexpm", "45d0cd46bd06738463fd53f22b70042dbb58c384bb99ef4e7576e7bb7d3b8c8c"},
}

0 comments on commit 6a1b28d

Please sign in to comment.