diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..23012a9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,67 @@ +# Contributing + +If you have found a bug, think I have misinterpreted the JSON schema spec +somewhere, or have a proposal for a new feature, feel free to open an issue so +we can discuss a proper solution. + +## Reporting bugs + +When reporting a bug, please include: + +- A short description of the bug, +- JSON schema example that triggers the bug, +- expected Elm output, and the +- actual Elm output. + +## Pull requests + +Please do not create pull requests before an issue has been created and a +solution has been discussed and agreed upon. + +When making a pull request ensure that: + +1. It solves one specific problem, and that problem has already been documented + and discussed as an issue, +2. the PR solves the problem as agreed upon in the issue, +3. if it is a new feature, ensure that there is proper code coverage of the new + feature, and +4. the PR contains no compiler warnings or dialyzer warnings (and preferably no + credo warnings). + +## Development + +The project is written in [Elixir](http://elixir-lang.org/) - as I found it to +be a more suitable tool for the job than Elm - and uses the `mix` tool for +building. + +#### Compiling + +Install dependencies + + mix deps.get + +Compile project + + mix compile + +and you are good to go. + +#### Tests + +Run the standard mix task + + mix test + +for test coverage run + + mix coveralls.html + +#### Static analysis + +Run dialyzer + + mix dialyzer + +Run credo + + mix credo diff --git a/README.md b/README.md index 5595a96..223c160 100644 --- a/README.md +++ b/README.md @@ -277,35 +277,51 @@ encodeCircle circle = ++ radius ``` -## Contributing +## Error reporting -If you feel like something is missing/wrong or if I've misinterpreted the JSON -schema spec, feel free to open an issue so we can discuss a solution. +Any errors encountered by the `js2e` tool while parsing the JSON schema files or +printing the Elm code output, is reported in an Elm-like style, e.g. + +``` +--- UNKNOWN NODE TYPE -------------------------------------- all_of_example.json + +The value of "type" at '#/allOf/0/properties/description' did not match a known node type + + "type": "strink" + ^^^^^^^^ -### Development +Was expecting one of the following types -As noted in the installation section, the project is written -in [Elixir](http://elixir-lang.org/) - as I found it to be a more suitable tool -for the job than Elm, and uses the `mix` tool for building. + ["null", "boolean", "object", "array", "number", "integer", "string"] -#### Compiling +Hint: See the specification section 6.25. "Validation keywords - type" + +``` -Install dependencies +or - mix deps.get +``` +--- UNRESOLVED REFERENCE ----------------------------------- all_of_example.json -Compile project - mix compile +The following reference at `#/allOf/0/color` could not be resolved -and you are good to go. + "$ref": #/definitions/kolor + ^^^^^^^^^^^^^^^^^^^ -#### Tests -Run the standard mix task +Hint: See the specification section 9. "Base URI and dereferencing" + +``` - mix test +If you encounter an error while using `js2e` that does not mimic the above +Elm-like style, but instead looks like an Elixir stacktrace, please report this +as a bug by opening an issue and includin a JSON schema example that recreates +the error. -for test coverage run +## Contributing + +If you feel like something is missing/wrong or if I've misinterpreted the JSON +schema spec, feel free to open an issue so we can discuss a solution. - mix coveralls.html +Please consult `CONTRIBUTING.md` first before opening an issue. diff --git a/lib/js2e.ex b/lib/js2e.ex index 70a63cd..4ce7d80 100644 --- a/lib/js2e.ex +++ b/lib/js2e.ex @@ -110,6 +110,7 @@ defmodule JS2E do def generate(schema_paths, module_name) do Logger.info("Parsing JSON schema files!") parser_result = Parser.parse_schema_files(schema_paths) + pretty_parser_warnings(parser_result.warnings) if length(parser_result.errors) > 0 do diff --git a/lib/parser/all_of_parser.ex b/lib/parser/all_of_parser.ex index 36e7b70..ae49126 100644 --- a/lib/parser/all_of_parser.ex +++ b/lib/parser/all_of_parser.ex @@ -30,7 +30,7 @@ defmodule JS2E.Parser.AllOfParser do """ require Logger - alias JS2E.Parser.{Util, ErrorUtil, ParserResult} + alias JS2E.Parser.{Util, ParserResult} alias JS2E.{Types, TypePath} alias JS2E.Types.AllOfType @@ -50,7 +50,7 @@ defmodule JS2E.Parser.AllOfParser do """ @impl JS2E.Parser.ParserBehaviour - @spec type?(Types.node()) :: boolean + @spec type?(Types.schemaNode()) :: boolean def type?(%{"allOf" => all_of}) when is_list(all_of) and length(all_of) > 0, do: true @@ -61,8 +61,13 @@ defmodule JS2E.Parser.AllOfParser do Parses a JSON schema allOf type into an `JS2E.Types.AllOfType`. """ @impl JS2E.Parser.ParserBehaviour - @spec parse(Types.node(), URI.t(), URI.t() | nil, TypePath.t(), String.t()) :: - ParserResult.t() + @spec parse( + Types.schemaNode(), + URI.t(), + URI.t() | nil, + TypePath.t(), + String.t() + ) :: ParserResult.t() def parse(%{"allOf" => all_of}, parent_id, id, path, name) when is_list(all_of) do child_path = TypePath.add_child(path, "allOf") diff --git a/lib/parser/error_util.ex b/lib/parser/error_util.ex index 63c9737..b05e418 100644 --- a/lib/parser/error_util.ex +++ b/lib/parser/error_util.ex @@ -128,8 +128,11 @@ defmodule JS2E.Parser.ErrorUtil do ParserError.new(identifier, :invalid_uri, error_msg) end - @spec unknown_node_type(Types.typeIdentifier(), String.t(), Types.node()) :: - ParserError.t() + @spec unknown_node_type( + Types.typeIdentifier(), + String.t(), + Types.schemaNode() + ) :: ParserError.t() def unknown_node_type(identifier, name, schema_node) do full_identifier = identifier @@ -178,12 +181,12 @@ defmodule JS2E.Parser.ErrorUtil do end end - @spec error_markings(String.t()) :: String.t() + @spec error_markings(String.t()) :: [String.t()] defp error_markings(value) do red(String.duplicate("^", String.length(value))) end - @spec red(String.t()) :: list + @spec red(String.t()) :: [String.t()] defp red(str) do IO.ANSI.format([:red, str]) end diff --git a/lib/parser/one_of_parser.ex b/lib/parser/one_of_parser.ex index fef69e5..158edf4 100644 --- a/lib/parser/one_of_parser.ex +++ b/lib/parser/one_of_parser.ex @@ -50,7 +50,7 @@ defmodule JS2E.Parser.OneOfParser do """ @impl JS2E.Parser.ParserBehaviour - @spec type?(Types.node()) :: boolean + @spec type?(Types.schemaNode()) :: boolean def type?(schema_node) do one_of = schema_node["oneOf"] is_list(one_of) && length(one_of) > 0 @@ -60,7 +60,7 @@ defmodule JS2E.Parser.OneOfParser do Parses a JSON schema oneOf type into an `JS2E.Types.OneOfType`. """ @impl JS2E.Parser.ParserBehaviour - @spec parse(Types.node(), URI.t(), URI.t(), TypePath.t(), String.t()) :: + @spec parse(Types.schemaNode(), URI.t(), URI.t(), TypePath.t(), String.t()) :: ParserResult.t() def parse(%{"oneOf" => one_of}, parent_id, id, path, name) when is_list(one_of) do diff --git a/lib/parser/parser.ex b/lib/parser/parser.ex index 921ffeb..8f69b2c 100644 --- a/lib/parser/parser.ex +++ b/lib/parser/parser.ex @@ -5,9 +5,7 @@ defmodule JS2E.Parser do """ require Logger - alias JS2E.Types alias JS2E.Parser.{RootParser, SchemaResult, ErrorUtil} - alias JS2E.Printers.Util @spec parse_schema_files([Path.t()]) :: SchemaResult.t() def parse_schema_files(schema_paths) do diff --git a/lib/parser/parser_result_types.ex b/lib/parser/parser_result_types.ex index da0d60b..fc23672 100644 --- a/lib/parser/parser_result_types.ex +++ b/lib/parser/parser_result_types.ex @@ -95,7 +95,7 @@ defmodule JS2E.Parser.ParserResult do type dictionaries to the list of errors in the merged `ParserResult`. """ - @spec merge(ParserResult.t(), ParserResult.t()) :: ParserResult.t() + @spec merge(t, t) :: t def merge( %__MODULE__{ type_dict: type_dict1, @@ -141,8 +141,8 @@ defmodule JS2E.Parser.SchemaResult do @type t :: %__MODULE__{ schema_dict: Types.schemaDictionary(), - warnings: [{Path.t(), ParserWarning.t()}], - errors: [{Path.t(), ParserError.t()}] + warnings: [{Path.t(), [ParserWarning.t()]}], + errors: [{Path.t(), [ParserError.t()]}] } defstruct [:schema_dict, :warnings, :errors] @@ -158,13 +158,9 @@ defmodule JS2E.Parser.SchemaResult do dictionary corresponding to the succesfully parsed JSON schema files, and a list of warnings and errors encountered while parsing. """ - @spec new( - [{Path.t(), Types.schemaDictionary()}], - [{Path.t(), ParserWarning.t()}], - [ - {Path.t(), ParserError.t()} - ] - ) :: t + @spec new(Types.schemaDictionary(), [{Path.t(), [ParserWarning.t()]}], [ + {Path.t(), [ParserError.t()]} + ]) :: t def new(schema_dict, warnings \\ [], errors \\ []) do %__MODULE__{schema_dict: schema_dict, warnings: warnings, errors: errors} end @@ -174,7 +170,7 @@ defmodule JS2E.Parser.SchemaResult do schema dictionaries to the list of errors in the merged `SchemaResult`. """ - @spec merge(SchemaResult.t(), SchemaResult.t()) :: SchemaResult.t() + @spec merge(t, t) :: t def merge( %__MODULE__{ schema_dict: schema_dict1, diff --git a/lib/parser/root_parser.ex b/lib/parser/root_parser.ex index a1d4c18..eca6771 100644 --- a/lib/parser/root_parser.ex +++ b/lib/parser/root_parser.ex @@ -6,13 +6,17 @@ defmodule JS2E.Parser.RootParser do require Logger alias JS2E.Parser.{ + AllOfParser, + AnyOfParser, ArrayParser, DefinitionsParser, ObjectParser, + OneOfParser, TupleParser, TypeReferenceParser, Util, ErrorUtil, + ParserError, ParserResult, SchemaResult } @@ -20,11 +24,11 @@ defmodule JS2E.Parser.RootParser do alias JS2E.{TypePath, Types} alias JS2E.Types.SchemaDefinition - @spec parse_schema(Types.schemaNode(), String.t()) :: SchemaResult.t() + @spec parse_schema(Types.schemaNode(), Path.t()) :: SchemaResult.t() def parse_schema(root_node, schema_file_path) do with {:ok, _schema_version} <- parse_schema_version(root_node), {:ok, schema_id} <- parse_schema_id(root_node) do - title = Map.get(root_node, "title", "") + title = Map.get(root_node, "title", "Root") description = Map.get(root_node, "description") root_node_no_def = Map.delete(root_node, "definitions") @@ -89,22 +93,33 @@ defmodule JS2E.Parser.RootParser do end @spec parse_root_object(map, URI.t(), String.t()) :: ParserResult.t() - defp parse_root_object(schema_root_node, schema_id, _title) do + defp parse_root_object(schema_root_node, schema_id, name) do type_path = TypePath.from_string("#") - name = "#" cond do + AllOfParser.type?(schema_root_node) -> + schema_root_node + |> AllOfParser.parse(schema_root_node, schema_id, type_path, name) + + AnyOfParser.type?(schema_root_node) -> + schema_root_node + |> AnyOfParser.parse(schema_root_node, schema_id, type_path, name) + ArrayParser.type?(schema_root_node) -> schema_root_node - |> Util.parse_type(schema_id, [], name) + |> ArrayParser.parse(schema_root_node, schema_id, type_path, name) ObjectParser.type?(schema_root_node) -> schema_root_node - |> Util.parse_type(schema_id, [], name) + |> ObjectParser.parse(schema_root_node, schema_id, type_path, name) + + OneOfParser.type?(schema_root_node) -> + schema_root_node + |> OneOfParser.parse(schema_root_node, schema_id, type_path, name) TupleParser.type?(schema_root_node) -> schema_root_node - |> Util.parse_type(schema_id, [], name) + |> TupleParser.parse(schema_root_node, schema_id, type_path, name) TypeReferenceParser.type?(schema_root_node) -> schema_root_node diff --git a/lib/parser/tuple_parser.ex b/lib/parser/tuple_parser.ex index 818baa8..8ea6527 100644 --- a/lib/parser/tuple_parser.ex +++ b/lib/parser/tuple_parser.ex @@ -34,7 +34,7 @@ defmodule JS2E.Parser.TupleParser do """ @impl JS2E.Parser.ParserBehaviour - @spec type?(Types.node()) :: boolean + @spec type?(Types.schemaNode()) :: boolean def type?(schema_node) do items = schema_node["items"] is_list(items) @@ -44,8 +44,13 @@ defmodule JS2E.Parser.TupleParser do Parses a JSON schema array type into an `JS2E.Types.TupleType`. """ @impl JS2E.Parser.ParserBehaviour - @spec parse(Types.node(), URI.t(), URI.t() | nil, TypePath.t(), String.t()) :: - ParserResult.t() + @spec parse( + Types.schemaNode(), + URI.t(), + URI.t() | nil, + TypePath.t(), + String.t() + ) :: ParserResult.t() def parse(%{"items" => items}, parent_id, id, path, name) when is_list(items) do child_path = TypePath.add_child(path, "items") @@ -65,10 +70,4 @@ defmodule JS2E.Parser.TupleParser do |> ParserResult.new() |> ParserResult.merge(child_types_result) end - - def parse(%{"items" => items}, _parent_id, _id, path, _name) do - items_type = ErrorUtil.get_type(items) - error = ErrorUtil.invalid_type(path, "items", "list or object", items_type) - ParserResult.new(%{}, [], [error]) - end end diff --git a/lib/parser/type_reference_parser.ex b/lib/parser/type_reference_parser.ex index 3f4f0c6..5078ed9 100644 --- a/lib/parser/type_reference_parser.ex +++ b/lib/parser/type_reference_parser.ex @@ -38,7 +38,7 @@ defmodule JS2E.Parser.TypeReferenceParser do @impl JS2E.Parser.ParserBehaviour @spec parse(map, URI.t(), URI.t() | nil, TypePath.t(), String.t()) :: ParserResult.t() - def parse(%{"$ref" => ref}, _parent_id, id, path, name) do + def parse(%{"$ref" => ref}, parent_id, id, path, name) do ref_path = ref |> to_type_identifier @@ -53,7 +53,7 @@ defmodule JS2E.Parser.TypeReferenceParser do @spec to_type_identifier(String.t()) :: Types.typeIdentifier() defp to_type_identifier(path) do if URI.parse(path).scheme != nil do - path |> URI.parse() + path |> URI.parse() |> to_string else path |> TypePath.from_string() end diff --git a/lib/parser/util.ex b/lib/parser/util.ex index 59613f1..edc829b 100644 --- a/lib/parser/util.ex +++ b/lib/parser/util.ex @@ -18,14 +18,13 @@ defmodule JS2E.Parser.Util do TypeReferenceParser, UnionParser, ErrorUtil, - ParserError, ParserResult } alias JS2E.{TypePath, Types} @type nodeParser :: - (Types.node(), URI.t(), URI.t(), TypePath.t(), String.t() -> + (Types.schemaNode(), URI.t(), URI.t(), TypePath.t(), String.t() -> ParserResult.t()) @doc ~S""" @@ -43,7 +42,7 @@ defmodule JS2E.Parser.Util do type_dict = if id != nil do string_id = - if type_def.name == "#" do + if string_path == "#" do "#{id}#" else "#{id}" @@ -66,10 +65,11 @@ defmodule JS2E.Parser.Util do def create_types_list(type_dict, path) do type_dict |> Enum.reduce(%{}, fn {child_abs_path, child_type}, reference_dict -> - child_type_path = TypePath.add_child(path, child_type.name) + normalized_child_name = child_type.name + child_type_path = TypePath.add_child(path, normalized_child_name) if child_type_path == TypePath.from_string(child_abs_path) do - Map.merge(reference_dict, %{child_type.name => child_type_path}) + Map.merge(reference_dict, %{normalized_child_name => child_type_path}) else reference_dict end @@ -100,23 +100,31 @@ defmodule JS2E.Parser.Util do definitions_result = if DefinitionsParser.type?(schema_node) do id = determine_id(schema_node, parent_id) - parent_id = determine_parent_id(id, parent_id) + child_parent_id = determine_parent_id(id, parent_id) type_path = TypePath.add_child(path, name) - DefinitionsParser.parse(schema_node, parent_id, id, type_path, name) + + DefinitionsParser.parse( + schema_node, + child_parent_id, + id, + type_path, + name + ) else ParserResult.new(%{}, [], []) end node_result = - case determine_node_parser(schema_node, path, name) do + case determine_node_parser(schema_node) do nil -> ParserResult.new(%{}, [], []) node_parser -> id = determine_id(schema_node, parent_id) - parent_id = determine_parent_id(id, parent_id) + child_parent_id = determine_parent_id(id, parent_id) type_path = TypePath.add_child(path, name) - node_parser.(schema_node, parent_id, id, type_path, name) + + node_parser.(schema_node, child_parent_id, id, type_path, name) end if Enum.empty?(definitions_result.type_dict) and @@ -128,12 +136,8 @@ defmodule JS2E.Parser.Util do end end - @spec determine_node_parser( - Types.schemaNode(), - Types.typeIdentifier(), - String.t() - ) :: nodeParser | nil - defp determine_node_parser(schema_node, identifier, name) do + @spec determine_node_parser(Types.schemaNode()) :: nodeParser | nil + defp determine_node_parser(schema_node) do predicate_node_type_pairs = [ {&AllOfParser.type?/1, &AllOfParser.parse/5}, {&AnyOfParser.type?/1, &AnyOfParser.parse/5}, @@ -147,24 +151,17 @@ defmodule JS2E.Parser.Util do {&UnionParser.type?/1, &UnionParser.parse/5} ] - node_parser = + {_pred?, node_parser} = predicate_node_type_pairs |> Enum.find({nil, nil}, fn {pred?, _node_parser} -> pred?.(schema_node) end) - |> elem(1) - if node_parser != nil do - node_parser - else - nil - end + node_parser end - @doc ~S""" - Determines the ID of a schema node based on its parent's ID and its own - optional '$id' or id' property. - """ + # Determines the ID of a schema node based on its parent's ID and its own + # optional '$id' or id' property. @spec determine_id(map, URI.t()) :: URI.t() | nil defp determine_id(%{"$id" => id}, parent_id) when is_binary(id) do do_determine_id(id, parent_id) @@ -189,7 +186,7 @@ defmodule JS2E.Parser.Util do @spec determine_parent_id(URI.t() | nil, URI.t()) :: URI.t() defp determine_parent_id(id, parent_id) do - if id != nil && id.scheme != "urn" do + if id != nil and id.scheme != "urn" do id else parent_id diff --git a/lib/printer/all_of_printer.ex b/lib/printer/all_of_printer.ex index 094a59f..7e3904a 100644 --- a/lib/printer/all_of_printer.ex +++ b/lib/printer/all_of_printer.ex @@ -5,9 +5,28 @@ defmodule JS2E.Printer.AllOfPrinter do """ require Elixir.{EEx, Logger} - alias JS2E.Printer.{Util, PrinterResult} + alias JS2E.Printer.{ErrorUtil, PrinterError, PrinterResult} + + alias JS2E.Printer.Utils.{ + Naming, + Indentation, + ElmTypes, + ElmDecoders, + ElmEncoders, + ResolveType, + CommonOperations + } + alias JS2E.{TypePath, Types} - alias JS2E.Types.{AllOfType, SchemaDefinition} + + alias JS2E.Types.{ + AllOfType, + EnumType, + OneOfType, + ObjectType, + UnionType, + SchemaDefinition + } @templates_location Application.get_env(:js2e, :templates_location) @@ -32,31 +51,22 @@ defmodule JS2E.Printer.AllOfPrinter do schema_dict, module_name ) do - type_name = Util.upcase_first(name) + normalized_name = Naming.normalize_identifier(name, :downcase) + type_name = Naming.upcase_first(normalized_name) {type_fields, errors} = types - |> create_type_fields(path, schema_def, schema_dict, module_name) - |> Util.split_ok_and_errors() + |> Enum.map( + &create_type_field(&1, path, schema_def, schema_dict, module_name) + ) + |> CommonOperations.split_ok_and_errors() type_name |> type_template(type_fields) |> PrinterResult.new(errors) end - @spec create_type_fields( - [TypePath.t()], - TypePath.t(), - SchemaDefinition.t(), - Types.schemaDictionary(), - String.t() - ) :: [{:ok, map} | {:error, PrinterError.t()}] - defp create_type_fields(types, parent, schema_def, schema_dict, module_name) do - types - |> Enum.map( - &create_type_field(&1, parent, schema_def, schema_dict, module_name) - ) - end + @type elm_type_field :: %{name: String.t(), type: String.t()} @spec create_type_field( TypePath.t(), @@ -64,7 +74,7 @@ defmodule JS2E.Printer.AllOfPrinter do SchemaDefinition.t(), Types.schemaDictionary(), String.t() - ) :: {:ok, map} | {:error, PrinterError.t()} + ) :: {:ok, elm_type_field} | {:error, PrinterError.t()} defp create_type_field( type_path, parent, @@ -74,13 +84,15 @@ defmodule JS2E.Printer.AllOfPrinter do ) do field_type_result = type_path - |> Util.resolve_type(parent, schema_def, schema_dict) - |> Util.create_type_name(schema_def, module_name) + |> ResolveType.resolve_type(parent, schema_def, schema_dict) + |> ElmTypes.create_type_name(schema_def, module_name) case field_type_result do {:ok, field_type} -> - field_name = Util.downcase_first(field_type) - {:ok, %{name: field_name, type: field_type}} + field_name = field_type |> Naming.normalize_identifier(:downcase) + field_type_name = Naming.upcase_first(field_name) + + {:ok, %{name: field_name, type: field_type_name}} {:error, error} -> {:error, error} @@ -109,39 +121,22 @@ defmodule JS2E.Printer.AllOfPrinter do schema_dict, module_name ) do - {clauses, errors} = + {decoder_clauses, errors} = type_paths - |> create_decoder_clauses(path, schema_def, schema_dict, module_name) - |> Util.split_ok_and_errors() + |> Enum.map( + &create_decoder_property(&1, path, schema_def, schema_dict, module_name) + ) + |> CommonOperations.split_ok_and_errors() - decoder_name = "#{name}Decoder" - type_name = Util.upcase_first(name) + normalized_name = Naming.normalize_identifier(name, :downcase) + decoder_name = "#{normalized_name}Decoder" + type_name = Naming.upcase_first(normalized_name) decoder_name - |> decoder_template(type_name, clauses) + |> decoder_template(type_name, decoder_clauses) |> PrinterResult.new(errors) end - @spec create_decoder_clauses( - [TypePath.t()], - TypePath.t(), - SchemaDefinition.t(), - Types.schemaDictionary(), - String.t() - ) :: [{:ok, map} | {:error, PrinterError.t()}] - defp create_decoder_clauses( - type_paths, - parent, - schema_def, - schema_dict, - module_name - ) do - type_paths - |> Enum.map( - &create_decoder_property(&1, parent, schema_def, schema_dict, module_name) - ) - end - @spec create_decoder_property( TypePath.t(), TypePath.t(), @@ -157,21 +152,18 @@ defmodule JS2E.Printer.AllOfPrinter do module_name ) do with {:ok, {property_type, resolved_schema_def}} <- - Util.resolve_type(type_path, parent, schema_def, schema_dict), + ResolveType.resolve_type(type_path, parent, schema_def, schema_dict), {:ok, decoder_name} <- - Util.create_decoder_name( + ElmDecoders.create_decoder_name( {:ok, {property_type, resolved_schema_def}}, schema_def, module_name ) do property_name = property_type.name - cond do - Util.union_type?(property_type) or Util.one_of_type?(property_type) -> - create_decoder_union_clause(property_name, decoder_name) - - Util.enum_type?(property_type) -> - case Util.determine_primitive_type_decoder(property_type.type) do + case property_type do + %EnumType{} -> + case ElmDecoders.determine_primitive_type_decoder(property_type.type) do {:ok, property_type_decoder} -> create_decoder_enum_clause( property_name, @@ -183,7 +175,13 @@ defmodule JS2E.Printer.AllOfPrinter do {:error, error} end - true -> + %OneOfType{} -> + create_decoder_union_clause(property_name, decoder_name) + + %UnionType{} -> + create_decoder_union_clause(property_name, decoder_name) + + _ -> create_decoder_normal_clause(property_name, decoder_name) end else @@ -214,7 +212,11 @@ defmodule JS2E.Printer.AllOfPrinter do @spec create_decoder_normal_clause(String.t(), String.t()) :: {:ok, map} defp create_decoder_normal_clause(property_name, decoder_name) do - {:ok, %{property_name: property_name, decoder_name: decoder_name}} + {:ok, + %{ + property_name: Naming.normalize_identifier(property_name, :downcase), + decoder_name: Naming.normalize_identifier(decoder_name, :downcase) + }} end # Encoder @@ -240,18 +242,18 @@ defmodule JS2E.Printer.AllOfPrinter do schema_dict, module_name ) do - {properties, errors} = + {encoder_properties, errors} = type_paths |> create_encoder_properties(path, schema_def, schema_dict, module_name) - |> Util.split_ok_and_errors() + |> CommonOperations.split_ok_and_errors() - type_name = Util.upcase_first(name) + argument_name = Naming.normalize_identifier(name, :downcase) + type_name = Naming.upcase_first(argument_name) encoder_name = "encode#{type_name}" - argument_name = Util.downcase_first(type_name) encoder_name - |> encoder_template(type_name, argument_name, properties) - |> Util.trim_newlines() + |> encoder_template(type_name, argument_name, encoder_properties) + |> Indentation.trim_newlines() |> PrinterResult.new(errors) end @@ -270,30 +272,71 @@ defmodule JS2E.Printer.AllOfPrinter do module_name ) do type_paths - |> Enum.map(&Util.resolve_type(&1, parent, schema_def, schema_dict)) + |> Enum.map(&ResolveType.resolve_type(&1, parent, schema_def, schema_dict)) |> Enum.map(&to_encoder_property(&1, schema_def, module_name)) + |> Enum.concat() end + @type elm_encoder :: %{ + name: String.t(), + parent_name: String.t(), + encoder_name: String.t(), + required: boolean + } + @spec to_encoder_property( {:ok, {Types.typeDefinition(), SchemaDefinition.t()}} | {:error, PrinterError.t()}, Types.schemaDictionary(), String.t() - ) :: {:ok, map} | {:error, PrinterError.t()} + ) :: [{:ok, elm_encoder} | {:error, PrinterError.t()}] defp to_encoder_property({:error, error}, _sf, _md), do: {:error, error} - defp to_encoder_property({:ok, {property, schema}}, schema_def, module_name) do - case Util.create_encoder_name( - {:ok, {property, schema}}, - schema_def, - module_name - ) do - {:ok, encoder_name} -> - updated_property = Map.put(property, :encoder_name, encoder_name) - {:ok, updated_property} + defp to_encoder_property( + {:ok, {%ObjectType{} = type_def, schema_def}}, + schema_dict, + module_name + ) do + parent_name = Naming.normalize_identifier(type_def.name) + required = type_def.required + + type_def.properties + |> Enum.map(fn {child_name, child_path} -> + with {:ok, {child_type_def, child_schema_def}} <- + ResolveType.resolve_type( + child_path, + type_def.path, + schema_def, + schema_dict + ), + {:ok, encoder_name} <- + ElmEncoders.create_encoder_name( + {:ok, {child_type_def, child_schema_def}}, + schema_def, + module_name + ) do + updated_child_property = + child_type_def + |> Map.put(:required, child_name in required) + |> Map.put(:encoder_name, encoder_name) + |> Map.put(:parent_name, parent_name) + + {:ok, updated_child_property} + else + {:error, error} -> + {:error, error} + end + end) + end - {:error, error} -> - {:error, error} - end + defp to_encoder_property( + {:ok, type_def, _schema_def}, + _schema_dict, + _module_name + ) do + error_msg = + "allOf printer expected ObjectType but found #{type_def.__struct__}" + + ErrorUtil.unexpected_type(type_def.path, error_msg) end end diff --git a/lib/printer/any_of_printer.ex b/lib/printer/any_of_printer.ex index f62227b..31a2be3 100644 --- a/lib/printer/any_of_printer.ex +++ b/lib/printer/any_of_printer.ex @@ -5,9 +5,28 @@ defmodule JS2E.Printer.AnyOfPrinter do """ require Elixir.{EEx, Logger} - alias JS2E.Printer.{Util, PrinterResult} + alias JS2E.Printer.{PrinterError, ErrorUtil, PrinterResult} + + alias JS2E.Printer.Utils.{ + Naming, + Indentation, + ElmTypes, + ElmDecoders, + ElmEncoders, + ResolveType, + CommonOperations + } + alias JS2E.{TypePath, Types} - alias JS2E.Types.{AnyOfType, SchemaDefinition} + + alias JS2E.Types.{ + AnyOfType, + EnumType, + ObjectType, + OneOfType, + UnionType, + SchemaDefinition + } @templates_location Application.get_env(:js2e, :templates_location) @@ -32,31 +51,21 @@ defmodule JS2E.Printer.AnyOfPrinter do schema_dict, module_name ) do - type_name = Util.upcase_first(name) + type_name = Naming.normalize_identifier(name, :upcase) {type_fields, errors} = types - |> create_type_fields(path, schema_def, schema_dict, module_name) - |> Util.split_ok_and_errors() + |> Enum.map( + &create_type_field(&1, path, schema_def, schema_dict, module_name) + ) + |> CommonOperations.split_ok_and_errors() type_name |> type_template(type_fields) |> PrinterResult.new(errors) end - @spec create_type_fields( - [TypePath.t()], - TypePath.t(), - SchemaDefinition.t(), - Types.schemaDictionary(), - String.t() - ) :: [{:ok, map} | {:error, PrinterError.t()}] - defp create_type_fields(types, parent, schema_def, schema_dict, module_name) do - types - |> Enum.map( - &create_type_field(&1, parent, schema_def, schema_dict, module_name) - ) - end + @type elm_type_field :: %{name: String.t(), type: String.t()} @spec create_type_field( TypePath.t(), @@ -64,7 +73,7 @@ defmodule JS2E.Printer.AnyOfPrinter do SchemaDefinition.t(), Types.schemaDictionary(), String.t() - ) :: {:ok, map} | {:error, PrinterErrot.t()} + ) :: {:ok, elm_type_field} | {:error, PrinterError.t()} defp create_type_field( type_path, parent, @@ -74,13 +83,15 @@ defmodule JS2E.Printer.AnyOfPrinter do ) do field_type_result = type_path - |> Util.resolve_type(parent, schema_def, schema_dict) - |> Util.create_type_name(schema_def, module_name) + |> ResolveType.resolve_type(parent, schema_def, schema_dict) + |> ElmTypes.create_type_name(schema_def, module_name) case field_type_result do {:ok, field_type} -> - field_name = Util.downcase_first(field_type) - {:ok, %{name: field_name, type: "Maybe #{field_type}"}} + field_name = field_type |> Naming.normalize_identifier(:downcase) + field_type_name = "Maybe #{Naming.upcase_first(field_name)}" + + {:ok, %{name: field_name, type: field_type_name}} {:error, error} -> {:error, error} @@ -111,37 +122,20 @@ defmodule JS2E.Printer.AnyOfPrinter do ) do {decoder_clauses, errors} = type_paths - |> create_decoder_clauses(path, schema_def, schema_dict, module_name) - |> Util.split_ok_and_errors() + |> Enum.map( + &create_decoder_property(&1, path, schema_def, schema_dict, module_name) + ) + |> CommonOperations.split_ok_and_errors() - decoder_name = "#{name}Decoder" - type_name = Util.upcase_first(name) + normalized_name = Naming.normalize_identifier(name, :downcase) + decoder_name = "#{normalized_name}Decoder" + type_name = Naming.upcase_first(normalized_name) decoder_name |> decoder_template(type_name, decoder_clauses) |> PrinterResult.new(errors) end - @spec create_decoder_clauses( - [TypePath.t()], - TypePath.t(), - SchemaDefinition.t(), - Types.schemaDictionary(), - String.t() - ) :: [{:ok, map} | {:error, PrinterError.t()}] - defp create_decoder_clauses( - type_paths, - parent, - schema_def, - schema_dict, - module_name - ) do - type_paths - |> Enum.map( - &create_decoder_property(&1, parent, schema_def, schema_dict, module_name) - ) - end - @spec create_decoder_property( TypePath.t(), TypePath.t(), @@ -157,21 +151,18 @@ defmodule JS2E.Printer.AnyOfPrinter do module_name ) do with {:ok, {property_type, resolved_schema_def}} <- - Util.resolve_type(type_path, parent, schema_def, schema_dict), + ResolveType.resolve_type(type_path, parent, schema_def, schema_dict), {:ok, decoder_name} <- - Util.create_decoder_name( + ElmDecoders.create_decoder_name( {:ok, {property_type, resolved_schema_def}}, schema_def, module_name ) do property_name = property_type.name - cond do - Util.union_type?(property_type) or Util.one_of_type?(property_type) -> - create_decoder_union_clause(property_name, decoder_name) - - Util.enum_type?(property_type) -> - case Util.determine_primitive_type_decoder(property_type.type) do + case property_type do + %EnumType{} -> + case ElmDecoders.determine_primitive_type_decoder(property_type.type) do {:ok, property_type_decoder} -> create_decoder_enum_clause( property_name, @@ -183,7 +174,13 @@ defmodule JS2E.Printer.AnyOfPrinter do {:error, error} end - true -> + %OneOfType{} -> + create_decoder_union_clause(property_name, decoder_name) + + %UnionType{} -> + create_decoder_union_clause(property_name, decoder_name) + + _ -> create_decoder_normal_clause(property_name, decoder_name) end else @@ -243,15 +240,15 @@ defmodule JS2E.Printer.AnyOfPrinter do {encoder_properties, errors} = type_paths |> create_encoder_properties(path, schema_def, schema_dict, module_name) - |> Util.split_ok_and_errors() + |> CommonOperations.split_ok_and_errors() - type_name = Util.upcase_first(name) + argument_name = Naming.normalize_identifier(name, :downcase) + type_name = Naming.upcase_first(argument_name) encoder_name = "encode#{type_name}" - argument_name = Util.downcase_first(type_name) encoder_name |> encoder_template(type_name, argument_name, encoder_properties) - |> Util.trim_newlines() + |> Indentation.trim_newlines() |> PrinterResult.new(errors) end @@ -261,7 +258,7 @@ defmodule JS2E.Printer.AnyOfPrinter do SchemaDefinition.t(), Types.schemaDictionary(), String.t() - ) :: {:ok, [map]} | {:error, PrinterError.t()} + ) :: [{:ok, [map]} | {:error, PrinterError.t()}] defp create_encoder_properties( type_paths, parent, @@ -270,30 +267,70 @@ defmodule JS2E.Printer.AnyOfPrinter do module_name ) do type_paths - |> Enum.map(&Util.resolve_type(&1, parent, schema_def, schema_dict)) + |> Enum.map(&ResolveType.resolve_type(&1, parent, schema_def, schema_dict)) |> Enum.map(&to_encoder_property(&1, schema_def, module_name)) + |> Enum.concat() end + @type elm_encoder :: %{ + name: String.t(), + encoder_name: String.t(), + required: boolean + } + @spec to_encoder_property( {:ok, {Types.typeDefinition(), SchemaDefinition.t()}} | {:error, PrinterError.t()}, Types.schemaDictionary(), String.t() - ) :: {:ok, map} | {:error, PrinterError.t()} + ) :: {:ok, elm_encoder} | {:error, PrinterError.t()} defp to_encoder_property({:error, error}, _sf, _md), do: {:error, error} - defp to_encoder_property({:ok, {property, schema}}, schema_def, module_name) do - case Util.create_encoder_name( - {:ok, {property, schema}}, - schema_def, - module_name - ) do - {:ok, encoder_name} -> - updated_property = Map.put(property, :encoder_name, encoder_name) - {:ok, updated_property} + defp to_encoder_property( + {:ok, {%ObjectType{} = type_def, schema_def}}, + schema_dict, + module_name + ) do + parent_name = Naming.normalize_identifier(type_def.name) + required = type_def.required + + type_def.properties + |> Enum.map(fn {child_name, child_path} -> + with {:ok, {child_type_def, child_schema_def}} <- + ResolveType.resolve_type( + child_path, + type_def.path, + schema_def, + schema_dict + ), + {:ok, encoder_name} <- + ElmEncoders.create_encoder_name( + {:ok, {child_type_def, child_schema_def}}, + schema_def, + module_name + ) do + updated_child_property = + child_type_def + |> Map.put(:required, child_name in required) + |> Map.put(:encoder_name, encoder_name) + |> Map.put(:parent_name, parent_name) + + {:ok, updated_child_property} + else + {:error, error} -> + {:error, error} + end + end) + end - {:error, error} -> - {:error, error} - end + defp to_encoder_property( + {:ok, type_def, _schema_def}, + _schema_dict, + _module_name + ) do + error_msg = + "anyOf printer expected ObjectType but found #{type_def.__struct__}" + + ErrorUtil.unexpected_type(type_def.path, error_msg) end end diff --git a/lib/printer/array_printer.ex b/lib/printer/array_printer.ex index 5f3b743..ce400c5 100644 --- a/lib/printer/array_printer.ex +++ b/lib/printer/array_printer.ex @@ -5,9 +5,18 @@ defmodule JS2E.Printer.ArrayPrinter do """ require Elixir.{EEx, Logger} - alias JS2E.Printer.{Util, PrinterResult} + alias JS2E.Printer.{PrinterError, PrinterResult} + + alias JS2E.Printer.Utils.{ + Naming, + ElmTypes, + ElmDecoders, + ElmEncoders, + ResolveType + } + alias JS2E.Types - alias JS2E.Types.{ArrayType, SchemaDefinition} + alias JS2E.Types.{ArrayType, PrimitiveType, SchemaDefinition} @templates_location Application.get_env(:js2e, :templates_location) @@ -52,11 +61,10 @@ defmodule JS2E.Printer.ArrayPrinter do _module_name ) do with {:ok, {items_type, _resolved_schema_def}} <- - Util.resolve_type(items_path, path, schema_def, schema_dict), + ResolveType.resolve_type(items_path, path, schema_def, schema_dict), {:ok, items_type_name} <- determine_type_name(items_type), {:ok, items_decoder_name} <- determine_decoder_name(items_type) do - "#{name}Decoder" - |> Util.downcase_first() + "#{Naming.normalize_identifier(name, :downcase)}Decoder" |> decoder_template(items_type_name, items_decoder_name) |> PrinterResult.new() else @@ -68,32 +76,37 @@ defmodule JS2E.Printer.ArrayPrinter do @spec determine_type_name(Types.typeDefinition()) :: {:ok, String.t()} | {:error, PrinterError.t()} defp determine_type_name(items_type) do - if Util.primitive_type?(items_type) do - Util.determine_primitive_type(items_type.type) - else - items_type_name = items_type.name - - if items_type_name == "#" do - {:ok, "Root"} - else - {:ok, Util.upcase_first(items_type_name)} - end + case items_type do + %PrimitiveType{} -> + ElmTypes.determine_primitive_type_name(items_type.type) + + _ -> + items_type_name = Naming.normalize_identifier(items_type.name, :upcase) + + if items_type_name == "Hash" do + {:ok, "Root"} + else + {:ok, items_type_name} + end end end @spec determine_decoder_name(Types.typeDefinition()) :: {:ok, String.t()} | {:error, PrinterError.t()} defp determine_decoder_name(items_type) do - if Util.primitive_type?(items_type) do - Util.determine_primitive_type_decoder(items_type.type) - else - items_type_name = items_type.name - - if items_type_name == "#" do - {:ok, "rootDecoder"} - else - {:ok, "#{items_type_name}Decoder"} - end + case items_type do + %PrimitiveType{} -> + ElmDecoders.determine_primitive_type_decoder(items_type.type) + + _ -> + items_type_name = + Naming.normalize_identifier(items_type.name, :downcase) + + if items_type_name == "hash" do + {:ok, "rootDecoder"} + else + {:ok, "#{items_type_name}Decoder"} + end end end @@ -121,10 +134,10 @@ defmodule JS2E.Printer.ArrayPrinter do _module_name ) do with {:ok, {items_type, _resolved_schema_def}} <- - Util.resolve_type(items_path, path, schema_def, schema_dict), + ResolveType.resolve_type(items_path, path, schema_def, schema_dict), {:ok, items_type_name} <- determine_type_name(items_type), {:ok, items_encoder_name} <- determine_encoder_name(items_type) do - "encode#{items_type_name}s" + "encode#{Naming.normalize_identifier(items_type_name, :upcase)}s" |> encoder_template(name, items_type_name, items_encoder_name) |> PrinterResult.new() else @@ -136,16 +149,18 @@ defmodule JS2E.Printer.ArrayPrinter do @spec determine_encoder_name(Types.typeDefinition()) :: {:ok, String.t()} | {:error, PrinterError.t()} defp determine_encoder_name(items_type) do - if Util.primitive_type?(items_type) do - Util.determine_primitive_type_encoder(items_type.type) - else - items_type_name = items_type.name - - if items_type_name == "#" do - {:ok, "encodeRoot"} - else - {:ok, "encode#{Util.upcase_first(items_type_name)}"} - end + case items_type do + %PrimitiveType{} -> + ElmEncoders.determine_primitive_type_encoder(items_type.type) + + _ -> + items_type_name = Naming.normalize_identifier(items_type.name, :upcase) + + if items_type_name == "Hash" do + {:ok, "encodeRoot"} + else + {:ok, "encode#{items_type_name}"} + end end end end diff --git a/lib/printer/enum_printer.ex b/lib/printer/enum_printer.ex index 67169c5..9ab720e 100644 --- a/lib/printer/enum_printer.ex +++ b/lib/printer/enum_printer.ex @@ -5,7 +5,8 @@ defmodule JS2E.Printer.EnumPrinter do """ require Elixir.{EEx, Logger} - alias JS2E.Printer.{Util, PrinterResult, ErrorUtil} + alias JS2E.Printer.{PrinterError, PrinterResult, ErrorUtil} + alias JS2E.Printer.Utils.{Naming, Indentation, ElmTypes, CommonOperations} alias JS2E.Types alias JS2E.Types.{EnumType, SchemaDefinition} @@ -35,10 +36,10 @@ defmodule JS2E.Printer.EnumPrinter do {clauses, errors} = values |> Enum.map(&create_elm_value(&1, type)) - |> Util.split_ok_and_errors() + |> CommonOperations.split_ok_and_errors() name - |> Util.upcase_first() + |> Naming.normalize_identifier(:upcase) |> type_template(clauses) |> PrinterResult.new(errors) end @@ -67,15 +68,15 @@ defmodule JS2E.Printer.EnumPrinter do _schema_dict, _module_name ) do - case Util.determine_primitive_type(type) do + case ElmTypes.determine_primitive_type_name(type) do {:ok, argument_type} -> - decoder_name = "#{Util.downcase_first(name)}Decoder" - decoder_type = Util.upcase_first(name) + decoder_name = "#{Naming.normalize_identifier(name, :downcase)}Decoder" + decoder_type = Naming.upcase_first(name) {decoder_cases, errors} = values |> create_decoder_cases(type) - |> Util.split_ok_and_errors() + |> CommonOperations.split_ok_and_errors() decoder_name |> decoder_template(decoder_type, name, argument_type, decoder_cases) @@ -142,17 +143,17 @@ defmodule JS2E.Printer.EnumPrinter do _schema_dict, _module_name ) do - argument_type = Util.upcase_first(name) + argument_type = Naming.normalize_identifier(name, :upcase) encoder_name = "encode#{argument_type}" {encoder_cases, errors} = values |> create_encoder_cases(type) - |> Util.split_ok_and_errors() + |> CommonOperations.split_ok_and_errors() encoder_name |> encoder_template(name, argument_type, encoder_cases) - |> Util.trim_newlines() + |> Indentation.trim_newlines() |> PrinterResult.new(errors) end @@ -200,7 +201,7 @@ defmodule JS2E.Printer.EnumPrinter do defp create_elm_value(value, type_name) do case type_name do "string" -> - {:ok, Util.upcase_first(value)} + {:ok, Naming.normalize_identifier(value, :upcase)} "integer" -> {:ok, "Int#{value}"} diff --git a/lib/printer/error_util.ex b/lib/printer/error_util.ex index 2c1a632..e2cfb6c 100644 --- a/lib/printer/error_util.ex +++ b/lib/printer/error_util.ex @@ -4,7 +4,7 @@ defmodule JS2E.Printer.ErrorUtil do """ alias JS2E.{Types, TypePath} - alias JS2E.Printer.{PrinterError} + alias JS2E.Printer.PrinterError @spec unresolved_reference( Types.typeIdentifier(), @@ -35,6 +35,11 @@ defmodule JS2E.Printer.ErrorUtil do PrinterError.new(type_name, :unknown_type, error_msg) end + @spec unexpected_type(Types.typeIdentifier(), String.t()) :: PrinterError.t() + def unexpected_type(identifier, error_msg) do + PrinterError.new(identifier, :unexpected_type, error_msg) + end + @spec unknown_enum_type(String.t()) :: PrinterError.t() def unknown_enum_type(type_name) do error_msg = "Unknown or unsupported enum type: '#{type_name}'" @@ -47,7 +52,7 @@ defmodule JS2E.Printer.ErrorUtil do PrinterError.new(type_name, :unknown_primitive_type, error_msg) end - @spec name_collision(String.t()) :: ParserError.t() + @spec name_collision(String.t()) :: PrinterError.t() def name_collision(file_name) do error_msg = "Found more than one schema with file: '#{file_name}'" PrinterError.new(file_name, :name_collision, error_msg) @@ -65,6 +70,9 @@ defmodule JS2E.Printer.ErrorUtil do @spec sanitize_value(any) :: String.t() defp sanitize_value(raw_value) do cond do + is_map(raw_value) and raw_value.__struct__ == URI -> + URI.to_string(raw_value) + is_map(raw_value) -> Poison.encode!(raw_value) @@ -76,12 +84,12 @@ defmodule JS2E.Printer.ErrorUtil do end end - @spec error_markings(String.t()) :: String.t() + @spec error_markings(String.t()) :: [String.t()] defp error_markings(value) do red(String.duplicate("^", String.length(value))) end - @spec red(String.t()) :: list + @spec red(String.t()) :: [String.t()] defp red(str) do IO.ANSI.format([:red, str]) end diff --git a/lib/printer/object_printer.ex b/lib/printer/object_printer.ex index 9e7011f..0ceb8f6 100644 --- a/lib/printer/object_printer.ex +++ b/lib/printer/object_printer.ex @@ -5,9 +5,27 @@ defmodule JS2E.Printer.ObjectPrinter do """ require Elixir.{EEx, Logger} - alias JS2E.Printer.{Util, PrinterResult} + alias JS2E.Printer.{PrinterError, PrinterResult} + + alias JS2E.Printer.Utils.{ + Naming, + Indentation, + ElmTypes, + ElmDecoders, + ElmEncoders, + ResolveType, + CommonOperations + } + alias JS2E.{TypePath, Types} - alias JS2E.Types.{ObjectType, SchemaDefinition} + + alias JS2E.Types.{ + EnumType, + OneOfType, + ObjectType, + UnionType, + SchemaDefinition + } @templates_location Application.get_env(:js2e, :templates_location) @@ -51,7 +69,7 @@ defmodule JS2E.Printer.ObjectPrinter do {fields, errors} = fields_result - |> Util.split_ok_and_errors() + |> CommonOperations.split_ok_and_errors() type_name |> type_template(fields) @@ -88,13 +106,15 @@ defmodule JS2E.Printer.ObjectPrinter do end @spec create_type_field( - {String.t(), String.t()}, + {String.t(), Types.typeIdentifier()}, [String.t()], TypePath.t(), SchemaDefinition.t(), Types.schemaDictionary(), String.t() - ) :: {:ok, map} | {:error, PrinterError.t()} + ) :: + {:ok, %{name: String.t(), type: String.t()}} + | {:error, PrinterError.t()} defp create_type_field( {property_name, property_path}, required, @@ -107,8 +127,8 @@ defmodule JS2E.Printer.ObjectPrinter do field_type_result = property_path - |> Util.resolve_type(properties_path, schema_def, schema_dict) - |> Util.create_type_name(schema_def, module_name) + |> ResolveType.resolve_type(properties_path, schema_def, schema_dict) + |> ElmTypes.create_type_name(schema_def, module_name) |> check_if_maybe(property_name, required) case field_type_result do @@ -123,8 +143,8 @@ defmodule JS2E.Printer.ObjectPrinter do @spec check_if_maybe( {:ok, String.t()} | {:error, PrinterError.t()}, String.t(), - boolean - ) :: String.t() + [String.t()] + ) :: {:ok, String.t()} | {:error, PrinterError.t()} defp check_if_maybe({:error, error}, _pn, _rq), do: {:error, error} defp check_if_maybe({:ok, field_name}, property_name, required) do @@ -163,7 +183,7 @@ defmodule JS2E.Printer.ObjectPrinter do module_name ) do type_name = create_root_name(name, schema_def) - decoder_name = "#{Util.downcase_first(type_name)}Decoder" + decoder_name = "#{Naming.downcase_first(type_name)}Decoder" {decoder_clauses, errors} = properties @@ -174,7 +194,7 @@ defmodule JS2E.Printer.ObjectPrinter do schema_dict, module_name ) - |> Util.split_ok_and_errors() + |> CommonOperations.split_ok_and_errors() decoder_name |> decoder_template(type_name, decoder_clauses) @@ -229,26 +249,23 @@ defmodule JS2E.Printer.ObjectPrinter do properties_path = TypePath.add_child(parent, property_name) with {:ok, {resolved_type, resolved_schema}} <- - Util.resolve_type( + ResolveType.resolve_type( property_path, properties_path, schema_def, schema_dict ), {:ok, decoder_name} <- - Util.create_decoder_name( + ElmDecoders.create_decoder_name( {:ok, {resolved_type, resolved_schema}}, schema_def, module_name ) do is_required = property_name in required - cond do - Util.union_type?(resolved_type) or Util.one_of_type?(resolved_type) -> - create_decoder_union_clause(property_name, decoder_name, is_required) - - Util.enum_type?(resolved_type) -> - case Util.determine_primitive_type_decoder(resolved_type.type) do + case resolved_type do + %EnumType{} -> + case ElmDecoders.determine_primitive_type_decoder(resolved_type.type) do {:ok, property_type_decoder} -> create_decoder_enum_clause( property_name, @@ -261,7 +278,13 @@ defmodule JS2E.Printer.ObjectPrinter do {:error, error} end - true -> + %OneOfType{} -> + create_decoder_union_clause(property_name, decoder_name, is_required) + + %UnionType{} -> + create_decoder_union_clause(property_name, decoder_name, is_required) + + _ -> create_decoder_normal_clause(property_name, decoder_name, is_required) end else @@ -367,7 +390,7 @@ defmodule JS2E.Printer.ObjectPrinter do ) do type_name = create_root_name(name, schema_def) encoder_name = "encode#{type_name}" - argument_name = Util.downcase_first(type_name) + argument_name = Naming.downcase_first(type_name) {encoder_properties, errors} = properties @@ -378,11 +401,11 @@ defmodule JS2E.Printer.ObjectPrinter do schema_dict, module_name ) - |> Util.split_ok_and_errors() + |> CommonOperations.split_ok_and_errors() encoder_name |> encoder_template(type_name, argument_name, encoder_properties) - |> Util.trim_newlines() + |> Indentation.trim_newlines() |> PrinterResult.new(errors) end @@ -434,14 +457,14 @@ defmodule JS2E.Printer.ObjectPrinter do properties_path = TypePath.add_child(parent, property_name) with {:ok, {resolved_type, resolved_schema}} <- - Util.resolve_type( + ResolveType.resolve_type( property_path, properties_path, schema_def, schema_dict ), {:ok, encoder_name} <- - Util.create_encoder_name( + ElmEncoders.create_encoder_name( {:ok, {resolved_type, resolved_schema}}, schema_def, module_name @@ -458,14 +481,16 @@ defmodule JS2E.Printer.ObjectPrinter do @spec create_root_name(String.t(), SchemaDefinition.t()) :: String.t() defp create_root_name(name, schema_def) do - if name == "#" do + normalized_name = Naming.normalize_identifier(name, :upcase) + + if normalized_name == "Hash" do if schema_def.title != nil do - Util.upcase_first(schema_def.title) + Naming.upcase_first(schema_def.title) else "Root" end else - Util.upcase_first(name) + normalized_name end end end diff --git a/lib/printer/one_of_printer.ex b/lib/printer/one_of_printer.ex index a832a67..139aa70 100644 --- a/lib/printer/one_of_printer.ex +++ b/lib/printer/one_of_printer.ex @@ -5,7 +5,8 @@ defmodule JS2E.Printer.OneOfPrinter do """ require Elixir.{EEx, Logger} - alias JS2E.Printer.{Util, PrinterResult} + alias JS2E.Printer.{PrinterError, PrinterResult} + alias JS2E.Printer.Utils.{Naming, Indentation, ResolveType, CommonOperations} alias JS2E.{TypePath, Types} alias JS2E.Types.{OneOfType, SchemaDefinition} @@ -32,12 +33,12 @@ defmodule JS2E.Printer.OneOfPrinter do schema_dict, _module_name ) do - type_name = Util.upcase_first(name) + type_name = Naming.normalize_identifier(name, :upcase) {type_clauses, errors} = types |> create_type_clauses(name, path, schema_def, schema_dict) - |> Util.split_ok_and_errors() + |> CommonOperations.split_ok_and_errors() type_name |> type_template(type_clauses) @@ -64,16 +65,21 @@ defmodule JS2E.Printer.OneOfPrinter do Types.schemaDictionary() ) :: {:ok, map} | {:error, PrinterError.t()} defp create_type_clause(type_clause_id, name, parent, schema_def, schema_dict) do - case Util.resolve_type(type_clause_id, parent, schema_def, schema_dict) do + case ResolveType.resolve_type( + type_clause_id, + parent, + schema_def, + schema_dict + ) do {:ok, {type_clause, _resolved_schema_def}} -> - type_value = Util.upcase_first(type_clause.name) + type_value = Naming.normalize_identifier(type_clause.name, :upcase) type_prefix = type_value |> String.slice(0..1) |> String.capitalize() - type_name = Util.upcase_first(name) + type_name = Naming.normalize_identifier(name, :upcase) {:ok, %{name: "#{type_name}#{type_prefix}", type: type_value}} @@ -107,10 +113,11 @@ defmodule JS2E.Printer.OneOfPrinter do {clause_decoders, errors} = types |> create_decoder_clauses(name, path, schema_def, schema_dict) - |> Util.split_ok_and_errors() + |> CommonOperations.split_ok_and_errors() - decoder_name = "#{name}Decoder" - decoder_type = Util.upcase_first(name) + normalized_name = Naming.normalize_identifier(name, :downcase) + decoder_name = "#{normalized_name}Decoder" + decoder_type = Naming.upcase_first(normalized_name) decoder_name |> decoder_template(decoder_type, clause_decoders) @@ -151,15 +158,21 @@ defmodule JS2E.Printer.OneOfPrinter do schema_def, schema_dict ) do - case Util.resolve_type(type_clause_id, parent, schema_def, schema_dict) do + case ResolveType.resolve_type( + type_clause_id, + parent, + schema_def, + schema_dict + ) do {:ok, {type_clause, _resolved_schema_def}} -> type_prefix = type_clause.name - |> Util.upcase_first() + |> Naming.normalize_identifier(:upcase) |> String.slice(0..1) |> String.capitalize() - success_name = "#{Util.upcase_first(name)}#{type_prefix}" + success_name = + "#{Naming.normalize_identifier(name, :upcase)}#{type_prefix}" {:ok, "#{type_clause.name}Decoder |> andThen (succeed << #{success_name})"} @@ -195,14 +208,14 @@ defmodule JS2E.Printer.OneOfPrinter do {encoder_cases, errors} = types |> create_encoder_cases(name, path, schema_def, schema_dict) - |> Util.split_ok_and_errors() + |> CommonOperations.split_ok_and_errors() - type_name = Util.upcase_first(name) + type_name = Naming.normalize_identifier(name, :upcase) encoder_name = "encode#{type_name}" encoder_name |> encoder_template(type_name, name, encoder_cases) - |> Util.trim_newlines() + |> Indentation.trim_newlines() |> PrinterResult.new(errors) end @@ -228,11 +241,11 @@ defmodule JS2E.Printer.OneOfPrinter do Types.schemaDictionary() ) :: {:ok, map} | {:error, PrinterError.t()} defp create_encoder_clause(type_path, name, parent, schema_def, schema_dict) do - case Util.resolve_type(type_path, parent, schema_def, schema_dict) do + case ResolveType.resolve_type(type_path, parent, schema_def, schema_dict) do {:ok, {clause_type, _resolved_schema_def}} -> - type_name = Util.upcase_first(name) - argument_name = clause_type.name - type_value = Util.upcase_first(argument_name) + type_name = Naming.normalize_identifier(name, :upcase) + argument_name = Naming.normalize_identifier(clause_type.name, :downcase) + type_value = Naming.upcase_first(argument_name) type_prefix = type_value diff --git a/lib/printer/preamble_printer.ex b/lib/printer/preamble_printer.ex index 667fb68..bda1ff6 100644 --- a/lib/printer/preamble_printer.ex +++ b/lib/printer/preamble_printer.ex @@ -7,7 +7,7 @@ defmodule JS2E.Printer.PreamblePrinter do @preamble_location Path.join(@templates_location, "preamble/preamble.elm.eex") require Elixir.{EEx, Logger} - alias JS2E.Printer.{PrinterResult, Util} + alias JS2E.Printer.PrinterResult alias JS2E.Types alias JS2E.Types.{TypeReference, SchemaDefinition} @@ -68,10 +68,12 @@ defmodule JS2E.Printer.PreamblePrinter do defp get_type_references(type_dict) do type_dict |> Enum.reduce([], fn {_path, type}, types -> - if Util.get_string_name(type) == "TypeReference" do - [type | types] - else - types + case type do + %TypeReference{} -> + [type | types] + + _ -> + types end end) end @@ -122,6 +124,14 @@ defmodule JS2E.Printer.PreamblePrinter do dependency_map |> Map.put(type_ref_schema_def.id, type_refs) + + true -> + Logger.error("Could not resolve #{inspect(type_ref)}") + Logger.error("with type_ref_uri #{to_string(type_ref_uri)}") + Logger.error("In dependency map #{inspect(dependency_map)}") + Logger.error("Where schema_uri #{inspect(schema_uri)}") + Logger.error("and schema_dict #{inspect(schema_dict)}") + dependency_map end end @@ -129,7 +139,7 @@ defmodule JS2E.Printer.PreamblePrinter do %{required(String.t()) => TypeReference.t()}, SchemaDefinition.t(), Types.schemaDictionary() - ) :: [{:ok, String.t()} | {:error, PrinterError.t()}] + ) :: [String.t()] defp create_dependencies(dependency_map, _schema_def, schema_dict) do dependency_map |> Enum.map(fn {schema_id, _type_refs} -> @@ -145,6 +155,8 @@ defmodule JS2E.Printer.PreamblePrinter do @spec has_same_absolute_path?(URI.t(), URI.t()) :: boolean defp has_same_absolute_path?(type_uri, schema_uri) do + # TODO: Should it be necessary that type_uri and schema_uri both have the + # same host? type_uri.host == schema_uri.host and type_uri.path == schema_uri.path end diff --git a/lib/printer/printer.ex b/lib/printer/printer.ex index 1eba77f..7a805e8 100644 --- a/lib/printer/printer.ex +++ b/lib/printer/printer.ex @@ -5,9 +5,44 @@ defmodule JS2E.Printer do """ require Logger - alias JS2E.Printer.{Util, PreamblePrinter, PrinterResult, SchemaResult} + + alias JS2E.Printer.{ + ErrorUtil, + PreamblePrinter, + PrinterResult, + SchemaResult + } + alias JS2E.Types - alias JS2E.Types.SchemaDefinition + + alias JS2E.Types.{ + AllOfType, + AnyOfType, + ArrayType, + EnumType, + ObjectType, + OneOfType, + PrimitiveType, + TupleType, + TypeReference, + UnionType, + SchemaDefinition + } + + alias JS2E.Printer.{ + ErrorUtil, + AllOfPrinter, + AnyOfPrinter, + ArrayPrinter, + EnumPrinter, + ObjectPrinter, + OneOfPrinter, + PrimitivePrinter, + TuplePrinter, + TypeReferencePrinter, + UnionPrinter, + PrinterResult + } @spec print_schemas(Types.schemaDictionary(), String.t()) :: SchemaResult def print_schemas(schema_dict, module_name \\ "") do @@ -55,13 +90,7 @@ defmodule JS2E.Printer do |> Enum.sort(&(&1.name < &2.name)) types_result = - merge_results( - values, - schema_def, - schema_dict, - module_name, - &Util.print_type/4 - ) + merge_results(values, schema_def, schema_dict, module_name, &print_type/4) decoders_result = merge_results( @@ -69,7 +98,7 @@ defmodule JS2E.Printer do schema_def, schema_dict, module_name, - &Util.print_decoder/4 + &print_decoder/4 ) encoders_result = @@ -78,7 +107,7 @@ defmodule JS2E.Printer do schema_def, schema_dict, module_name, - &Util.print_encoder/4 + &print_encoder/4 ) printer_result = @@ -91,7 +120,7 @@ defmodule JS2E.Printer do |> Map.put(:printed_schema, printer_result.printed_schema <> "\n") end - @spec filter_aliases(Types.typeDictionary()) :: Types.typeDictionary() + @spec filter_aliases(Types.typeDictionary()) :: [Types.typeDefinition()] defp filter_aliases(type_dict) do type_dict |> Enum.reduce([], fn {path, value}, values -> @@ -103,12 +132,19 @@ defmodule JS2E.Printer do end) end + @type process_fun :: + (Types.typeDefinition(), + SchemaDefinition.t(), + Types.schemaDictionary(), + String.t() -> + PrinterResult.t()) + @spec merge_results( - Types.typeDictionary(), + [Types.typeDefinition()], SchemaDefinition.t(), Types.schemaDictionary(), String.t(), - fun + process_fun ) :: PrinterResult.t() defp merge_results(values, schema_def, schema_dict, module_name, process_fun) do values @@ -117,4 +153,254 @@ defmodule JS2E.Printer do PrinterResult.merge(acc, type_result) end) end + + @spec print_type( + Types.typeDefinition(), + SchemaDefinition.t(), + Types.schemaDictionary(), + String.t() + ) :: PrinterResult.t() + def print_type(type_def, schema_def, schema_dict, module_name) do + case type_def do + %AllOfType{} -> + AllOfPrinter.print_type(type_def, schema_def, schema_dict, module_name) + + %AnyOfType{} -> + AnyOfPrinter.print_type(type_def, schema_def, schema_dict, module_name) + + %ArrayType{} -> + ArrayPrinter.print_type(type_def, schema_def, schema_dict, module_name) + + %EnumType{} -> + EnumPrinter.print_type(type_def, schema_def, schema_dict, module_name) + + %ObjectType{} -> + ObjectPrinter.print_type(type_def, schema_def, schema_dict, module_name) + + %OneOfType{} -> + OneOfPrinter.print_type(type_def, schema_def, schema_dict, module_name) + + %PrimitiveType{} -> + PrimitivePrinter.print_type( + type_def, + schema_def, + schema_dict, + module_name + ) + + %TupleType{} -> + TuplePrinter.print_type(type_def, schema_def, schema_dict, module_name) + + %TypeReference{} -> + TypeReferencePrinter.print_type( + type_def, + schema_def, + schema_dict, + module_name + ) + + %UnionType{} -> + UnionPrinter.print_type(type_def, schema_def, schema_dict, module_name) + + _ -> + struct_name = get_struct_name(type_def) + PrinterResult.new("", [ErrorUtil.unknown_type(struct_name)]) + end + end + + @spec print_decoder( + Types.typeDefinition(), + SchemaDefinition.t(), + Types.schemaDictionary(), + String.t() + ) :: PrinterResult.t() + def print_decoder(type_def, schema_def, schema_dict, module_name) do + case type_def do + %AllOfType{} -> + AllOfPrinter.print_decoder( + type_def, + schema_def, + schema_dict, + module_name + ) + + %AnyOfType{} -> + AnyOfPrinter.print_decoder( + type_def, + schema_def, + schema_dict, + module_name + ) + + %ArrayType{} -> + ArrayPrinter.print_decoder( + type_def, + schema_def, + schema_dict, + module_name + ) + + %EnumType{} -> + EnumPrinter.print_decoder( + type_def, + schema_def, + schema_dict, + module_name + ) + + %ObjectType{} -> + ObjectPrinter.print_decoder( + type_def, + schema_def, + schema_dict, + module_name + ) + + %OneOfType{} -> + OneOfPrinter.print_decoder( + type_def, + schema_def, + schema_dict, + module_name + ) + + %PrimitiveType{} -> + PrimitivePrinter.print_decoder( + type_def, + schema_def, + schema_dict, + module_name + ) + + %TupleType{} -> + TuplePrinter.print_decoder( + type_def, + schema_def, + schema_dict, + module_name + ) + + %TypeReference{} -> + TypeReferencePrinter.print_decoder( + type_def, + schema_def, + schema_dict, + module_name + ) + + %UnionType{} -> + UnionPrinter.print_decoder( + type_def, + schema_def, + schema_dict, + module_name + ) + + _ -> + struct_name = get_struct_name(type_def) + PrinterResult.new("", [ErrorUtil.unknown_type(struct_name)]) + end + end + + @spec print_encoder( + Types.typeDefinition(), + SchemaDefinition.t(), + Types.schemaDictionary(), + String.t() + ) :: PrinterResult.t() + def print_encoder(type_def, schema_def, schema_dict, module_name) do + case type_def do + %AllOfType{} -> + AllOfPrinter.print_encoder( + type_def, + schema_def, + schema_dict, + module_name + ) + + %AnyOfType{} -> + AnyOfPrinter.print_encoder( + type_def, + schema_def, + schema_dict, + module_name + ) + + %ArrayType{} -> + ArrayPrinter.print_encoder( + type_def, + schema_def, + schema_dict, + module_name + ) + + %EnumType{} -> + EnumPrinter.print_encoder( + type_def, + schema_def, + schema_dict, + module_name + ) + + %ObjectType{} -> + ObjectPrinter.print_encoder( + type_def, + schema_def, + schema_dict, + module_name + ) + + %OneOfType{} -> + OneOfPrinter.print_encoder( + type_def, + schema_def, + schema_dict, + module_name + ) + + %PrimitiveType{} -> + PrimitivePrinter.print_encoder( + type_def, + schema_def, + schema_dict, + module_name + ) + + %TupleType{} -> + TuplePrinter.print_encoder( + type_def, + schema_def, + schema_dict, + module_name + ) + + %TypeReference{} -> + TypeReferencePrinter.print_encoder( + type_def, + schema_def, + schema_dict, + module_name + ) + + %UnionType{} -> + UnionPrinter.print_encoder( + type_def, + schema_def, + schema_dict, + module_name + ) + + _ -> + struct_name = get_struct_name(type_def) + PrinterResult.new("", [ErrorUtil.unknown_type(struct_name)]) + end + end + + @spec get_struct_name(struct) :: String.t() + defp get_struct_name(struct) do + struct.__struct__ + |> to_string + |> String.split(".") + |> List.last() + end end diff --git a/lib/printer/printer_behaviour.ex b/lib/printer/printer_behaviour.ex index b0a6464..0363e94 100644 --- a/lib/printer/printer_behaviour.ex +++ b/lib/printer/printer_behaviour.ex @@ -3,6 +3,7 @@ defmodule JS2E.Printer.PrinterBehaviour do Describes the functions needed to implement a printer of a JSON schema node. """ + alias JS2E.Printer.PrinterResult alias JS2E.Types alias JS2E.Types.SchemaDefinition @@ -11,19 +12,19 @@ defmodule JS2E.Printer.PrinterBehaviour do SchemaDefinition.t(), Types.schemaDictionary(), String.t() - ) :: String.t() + ) :: PrinterResult.t() @callback print_decoder( Types.typeDefinition(), SchemaDefinition.t(), Types.schemaDictionary(), String.t() - ) :: String.t() + ) :: PrinterResult.t() @callback print_encoder( Types.typeDefinition(), SchemaDefinition.t(), Types.schemaDictionary(), String.t() - ) :: String.t() + ) :: PrinterResult.t() end diff --git a/lib/printer/printer_result_types.ex b/lib/printer/printer_result_types.ex index db2aa7b..301c515 100644 --- a/lib/printer/printer_result_types.ex +++ b/lib/printer/printer_result_types.ex @@ -59,7 +59,7 @@ defmodule JS2E.Printer.PrinterResult do dictionaries to the list of errors in the merged `PrinterResult`. """ - @spec merge(PrinterResult.t(), PrinterResult.t()) :: PrinterResult.t() + @spec merge(t, t) :: t def merge( %__MODULE__{printed_schema: printed_schema1, errors: errors1}, %__MODULE__{ @@ -112,7 +112,7 @@ defmodule JS2E.Printer.SchemaResult do dictionaries to the list of errors in the merged `SchemaResult`. """ - @spec merge(SchemaResult.t(), SchemaResult.t()) :: SchemaResult.t() + @spec merge(t, t) :: t def merge(%__MODULE__{file_dict: file_dict1, errors: errors1}, %__MODULE__{ file_dict: file_dict2, errors: errors2 diff --git a/lib/printer/tuple_printer.ex b/lib/printer/tuple_printer.ex index 58a79c3..18d069e 100644 --- a/lib/printer/tuple_printer.ex +++ b/lib/printer/tuple_printer.ex @@ -5,9 +5,20 @@ defmodule JS2E.Printer.TuplePrinter do """ require Elixir.{EEx, Logger} - alias JS2E.Printer.{Util, PrinterResult} + alias JS2E.Printer.{PrinterError, PrinterResult} + + alias JS2E.Printer.Utils.{ + Naming, + Indentation, + ElmTypes, + ElmDecoders, + ElmEncoders, + ResolveType, + CommonOperations + } + alias JS2E.{TypePath, Types} - alias JS2E.Types.{TupleType, SchemaDefinition} + alias JS2E.Types.{EnumType, OneOfType, TupleType, UnionType, SchemaDefinition} @templates_location Application.get_env(:js2e, :templates_location) @@ -35,10 +46,10 @@ defmodule JS2E.Printer.TuplePrinter do {type_fields, errors} = types |> create_type_fields(path, schema_def, schema_dict, module_name) - |> Util.split_ok_and_errors() + |> CommonOperations.split_ok_and_errors() name - |> Util.upcase_first() + |> Naming.normalize_identifier(:upcase) |> type_template(type_fields) |> PrinterResult.new(errors) end @@ -72,8 +83,8 @@ defmodule JS2E.Printer.TuplePrinter do module_name ) do type_path - |> Util.resolve_type(parent, schema_def, schema_dict) - |> Util.create_type_name(schema_def, module_name) + |> ResolveType.resolve_type(parent, schema_def, schema_dict) + |> ElmTypes.create_type_name(schema_def, module_name) end # Decoder @@ -101,10 +112,11 @@ defmodule JS2E.Printer.TuplePrinter do {decoder_clauses, errors} = type_paths |> create_decoder_clauses(path, schema_def, schema_dict, module_name) - |> Util.split_ok_and_errors() + |> CommonOperations.split_ok_and_errors() - decoder_name = "#{name}Decoder" - type_name = Util.upcase_first(name) + normalized_name = Naming.normalize_identifier(name, :downcase) + decoder_name = "#{normalized_name}Decoder" + type_name = Naming.upcase_first(normalized_name) decoder_name |> decoder_template(type_name, decoder_clauses) @@ -146,19 +158,16 @@ defmodule JS2E.Printer.TuplePrinter do module_name ) do with {:ok, {property_type, resolved_schema_def}} <- - Util.resolve_type(type_path, parent, schema_def, schema_dict), + ResolveType.resolve_type(type_path, parent, schema_def, schema_dict), {:ok, decoder_name} <- - Util.create_decoder_name( + ElmDecoders.create_decoder_name( {:ok, {property_type, resolved_schema_def}}, schema_def, module_name ) do - cond do - Util.union_type?(property_type) or Util.one_of_type?(property_type) -> - create_decoder_union_clause(decoder_name) - - Util.enum_type?(property_type) -> - case Util.determine_primitive_type_decoder(property_type.type) do + case property_type do + %EnumType{} -> + case ElmDecoders.determine_primitive_type_decoder(property_type.type) do {:ok, property_type_decoder} -> create_decoder_enum_clause(property_type_decoder, decoder_name) @@ -166,7 +175,13 @@ defmodule JS2E.Printer.TuplePrinter do {:error, error} end - true -> + %OneOfType{} -> + create_decoder_union_clause(decoder_name) + + %UnionType{} -> + create_decoder_union_clause(decoder_name) + + _ -> create_decoder_normal_clause(decoder_name) end else @@ -216,14 +231,14 @@ defmodule JS2E.Printer.TuplePrinter do {encoder_properties, errors} = type_paths |> create_encoder_properties(path, schema_def, schema_dict, module_name) - |> Util.split_ok_and_errors() + |> CommonOperations.split_ok_and_errors() - type_name = Util.upcase_first(name) + type_name = Naming.normalize_identifier(name, :upcase) encoder_name = "encode#{type_name}" encoder_name |> encoder_template(type_name, encoder_properties) - |> Util.trim_newlines() + |> Indentation.trim_newlines() |> PrinterResult.new(errors) end @@ -242,7 +257,7 @@ defmodule JS2E.Printer.TuplePrinter do module_name ) do type_paths - |> Enum.map(&Util.resolve_type(&1, parent, schema_def, schema_dict)) + |> Enum.map(&ResolveType.resolve_type(&1, parent, schema_def, schema_dict)) |> Enum.map(&to_encoder_property(&1, schema_def, module_name)) end @@ -260,7 +275,7 @@ defmodule JS2E.Printer.TuplePrinter do module_name ) do encoder_name_result = - Util.create_encoder_name( + ElmEncoders.create_encoder_name( {:ok, {resolved_property, resolved_schema}}, schema_def, module_name diff --git a/lib/printer/union_printer.ex b/lib/printer/union_printer.ex index 9207cf8..05b48fb 100644 --- a/lib/printer/union_printer.ex +++ b/lib/printer/union_printer.ex @@ -5,7 +5,8 @@ defmodule JS2E.Printer.UnionPrinter do """ require Elixir.{EEx, Logger} - alias JS2E.Printer.{Util, PrinterResult, ErrorUtil} + alias JS2E.Printer.{PrinterError, PrinterResult, ErrorUtil} + alias JS2E.Printer.Utils.{Naming, Indentation, CommonOperations} alias JS2E.{Types} alias JS2E.Types.{UnionType, SchemaDefinition} @@ -35,10 +36,10 @@ defmodule JS2E.Printer.UnionPrinter do {type_clauses, errors} = types |> create_type_clauses(name) - |> Util.split_ok_and_errors() + |> CommonOperations.split_ok_and_errors() name - |> Util.upcase_first() + |> Naming.normalize_identifier(:upcase) |> type_template(type_clauses) |> PrinterResult.new(errors) end @@ -55,7 +56,7 @@ defmodule JS2E.Printer.UnionPrinter do @spec to_type_clause(String.t(), String.t()) :: {:ok, map} | {:error, PrinterError.t()} defp to_type_clause(type_id, name) do - type_name = Util.upcase_first(name) + type_name = Naming.normalize_identifier(name, :upcase) case type_id do "boolean" -> @@ -98,15 +99,16 @@ defmodule JS2E.Printer.UnionPrinter do _schema_dict, _module_name ) do - decoder_name = "#{name}Decoder" - type_name = Util.upcase_first(name) + normalized_name = Naming.normalize_identifier(name, :downcase) + decoder_name = "#{normalized_name}Decoder" + type_name = Naming.upcase_first(normalized_name) nullable? = "null" in types decoder_type = check_if_maybe(type_name, nullable?) {decoder_clauses, errors} = types |> create_clause_decoders(type_name, nullable?) - |> Util.split_ok_and_errors() + |> CommonOperations.split_ok_and_errors() decoder_name |> decoder_template(decoder_type, nullable?, decoder_clauses) @@ -201,14 +203,14 @@ defmodule JS2E.Printer.UnionPrinter do {encoder_cases, errors} = types |> create_encoder_cases(name) - |> Util.split_ok_and_errors() + |> CommonOperations.split_ok_and_errors() - type_name = Util.upcase_first(name) + type_name = Naming.normalize_identifier(name, :upcase) encoder_name = "encode#{type_name}" encoder_name |> encoder_template(type_name, name, encoder_cases) - |> Util.trim_newlines() + |> Indentation.trim_newlines() |> PrinterResult.new(errors) end @@ -243,7 +245,8 @@ defmodule JS2E.Printer.UnionPrinter do case type_id_result do {:ok, {constructor_suffix, encoder_name, argument_name}} -> - constructor_name = Util.upcase_first(name) <> constructor_suffix + constructor_name = + Naming.normalize_identifier(name, :upcase) <> constructor_suffix {:ok, %{ diff --git a/lib/printer/util.ex b/lib/printer/util.ex deleted file mode 100644 index 99b26b5..0000000 --- a/lib/printer/util.ex +++ /dev/null @@ -1,642 +0,0 @@ -defmodule JS2E.Printer.Util do - @moduledoc ~S""" - A module containing utility function for JSON schema printers. - """ - - require Logger - alias JS2E.{Types, TypePath} - alias JS2E.Types.{PrimitiveType, SchemaDefinition} - - alias JS2E.Printer.{ - ErrorUtil, - AllOfPrinter, - AnyOfPrinter, - ArrayPrinter, - EnumPrinter, - ObjectPrinter, - OneOfPrinter, - PrimitivePrinter, - TuplePrinter, - TypeReferencePrinter, - UnionPrinter, - PrinterResult - } - - # Indentation, whitespace and casing - start - - @indent_size 4 - - @doc ~S""" - Returns a chunk of indentation. - - ## Examples - - iex> indent(2) - " " - - """ - @spec indent(pos_integer) :: String.t() - def indent(tabs \\ 1) when is_integer(tabs) do - String.pad_leading("", tabs * @indent_size) - end - - @doc ~S""" - Remove excessive newlines of a string. - """ - @spec trim_newlines(String.t()) :: String.t() - def trim_newlines(str) do - String.trim(str) <> "\n" - end - - @doc ~S""" - Upcases the first letter of a string. - - ## Examples - - iex> upcase_first("foobar") - "Foobar" - - """ - @spec upcase_first(String.t()) :: String.t() - def upcase_first(string) when is_binary(string) do - if String.length(string) > 0 do - String.upcase(String.at(string, 0)) <> String.slice(string, 1..-1) - else - "" - end - end - - @doc ~S""" - Upcases the first letter of a string. - - ## Examples - - iex> downcase_first("Foobar") - "foobar" - - """ - @spec downcase_first(String.t()) :: String.t() - def downcase_first(string) when is_binary(string) do - if String.length(string) > 0 do - String.downcase(String.at(string, 0)) <> String.slice(string, 1..-1) - else - "" - end - end - - # Indentation, whitespace and casing - end - - # Printing types - start - - @spec create_type_name( - {:ok, {Types.typeDefinition(), SchemaDefinition.t()}} - | {:error, PrinterError.t()}, - SchemaDefinition.t(), - String.t() - ) :: {:ok, String.t()} | {:error, PrinterError.t()} - def create_type_name({:error, error}, _schema, _name), do: {:error, error} - - def create_type_name( - {:ok, {resolved_type, resolved_schema}}, - context_schema, - module_name - ) do - type_name = - if primitive_type?(resolved_type) do - determine_primitive_type(resolved_type.type) - else - resolved_type_name = resolved_type.name - - if resolved_type_name == "#" do - if resolved_schema.title != nil do - {:ok, upcase_first(resolved_schema.title)} - else - {:ok, "Root"} - end - else - {:ok, upcase_first(resolved_type_name)} - end - end - - case type_name do - {:ok, type_name} -> - if resolved_schema.id != context_schema.id do - {:ok, qualify_name(resolved_schema, type_name, module_name)} - else - {:ok, type_name} - end - - {:error, error} -> - {:error, error} - end - end - - @doc ~S""" - Converts the following primitive types: "string", "integer", "number", - and "boolean" into their Elm type equivalent. Raises and error otherwise. - - ## Examples - - iex> determine_primitive_type("string") - {:ok, "String"} - - iex> determine_primitive_type("integer") - {:ok, "Int"} - - iex> determine_primitive_type("number") - {:ok, "Float"} - - iex> determine_primitive_type("boolean") - {:ok, "Bool"} - - iex> {:error, error} = determine_primitive_type("array") - iex> error.error_type - :unknown_primitive_type - - """ - @spec determine_primitive_type(String.t()) :: - {:ok, String.t()} | {:error, PrinterError.t()} - def determine_primitive_type(type_name) do - case type_name do - "string" -> - {:ok, "String"} - - "integer" -> - {:ok, "Int"} - - "number" -> - {:ok, "Float"} - - "boolean" -> - {:ok, "Bool"} - - _ -> - {:error, ErrorUtil.unknown_primitive_type(type_name)} - end - end - - # Printing types - end - - # Printing decoders - start - - @spec create_decoder_name( - {:ok, {Types.typeDefinition(), SchemaDefinition.t()}} - | {:error, PrinterError.t()}, - SchemaDefinition.t(), - String.t() - ) :: {:ok, String.t()} | {:error, PrinterError.t()} - def create_decoder_name({:error, error}, _schema, _name), do: {:error, error} - - def create_decoder_name( - {:ok, {resolved_type, resolved_schema}}, - context_schema, - module_name - ) do - decoder_name = - if primitive_type?(resolved_type) do - determine_primitive_type_decoder(resolved_type.type) - else - type_name = resolved_type.name - - if type_name == "#" do - if resolved_schema.title != nil do - {:ok, "#{downcase_first(resolved_schema.title)}Decoder"} - else - {:ok, "rootDecoder"} - end - else - {:ok, "#{type_name}Decoder"} - end - end - - case decoder_name do - {:ok, decoder_name} -> - if resolved_schema.id != context_schema.id do - {:ok, qualify_name(resolved_schema, decoder_name, module_name)} - else - {:ok, decoder_name} - end - - {:error, error} -> - {:error, error} - end - end - - @doc ~S""" - Converts the following primitive types: "string", "integer", "number", - and "boolean" into their Elm decoder equivalent. Raises an error otherwise. - - ## Examples - - iex> determine_primitive_type_decoder("string") - {:ok, "Decode.string"} - - iex> determine_primitive_type_decoder("integer") - {:ok, "Decode.int"} - - iex> determine_primitive_type_decoder("number") - {:ok, "Decode.float"} - - iex> determine_primitive_type_decoder("boolean") - {:ok, "Decode.bool"} - - iex> {:error, error} = determine_primitive_type_decoder("array") - iex> error.error_type - :unknown_primitive_type - - """ - @spec determine_primitive_type_decoder(String.t()) :: - {:ok, String.t()} | {:error, PrinterError.t()} - def determine_primitive_type_decoder(type_name) do - case type_name do - "string" -> - {:ok, "Decode.string"} - - "integer" -> - {:ok, "Decode.int"} - - "number" -> - {:ok, "Decode.float"} - - "boolean" -> - {:ok, "Decode.bool"} - - _ -> - {:error, ErrorUtil.unknown_primitive_type(type_name)} - end - end - - # Printing decoders - end - - # Printing encoders - start - - @doc ~S""" - Returns the encoder name given a JSON schema type definition. - """ - @spec create_encoder_name( - {:ok, {Types.typeDefinition(), SchemaDefinition.t()}} - | {:error, PrinterError.t()}, - SchemaDefinition.t(), - String.t() - ) :: {:ok, String.t()} | {:error, PrinterError.t()} - def create_encoder_name({:error, error}, _schema, _name), do: {:error, error} - - def create_encoder_name( - {:ok, {resolved_type, resolved_schema}}, - context_schema, - module_name - ) do - encoder_name_result = - if primitive_type?(resolved_type) do - determine_primitive_type_encoder(resolved_type.type) - else - type_name = resolved_type.name - - if type_name == "#" do - if resolved_schema.title != nil do - {:ok, "encode#{upcase_first(resolved_schema.title)}"} - else - {:ok, "encodeRoot"} - end - else - {:ok, "encode#{upcase_first(type_name)}"} - end - end - - case encoder_name_result do - {:ok, encoder_name} -> - if resolved_schema.id != context_schema.id do - {:ok, qualify_name(resolved_schema, encoder_name, module_name)} - else - {:ok, encoder_name} - end - - {:error, error} -> - {:error, error} - end - end - - @doc ~S""" - Converts the following primitive types: "string", "integer", "number", - "boolean", and "null" into their Elm encoder equivalent. Raises an error - otherwise. - - ## Examples - - iex> determine_primitive_type_encoder("string") - {:ok, "Encode.string"} - - iex> determine_primitive_type_encoder("integer") - {:ok, "Encode.int"} - - iex> determine_primitive_type_encoder("number") - {:ok, "Encode.float"} - - iex> determine_primitive_type_encoder("boolean") - {:ok, "Encode.bool"} - - iex> determine_primitive_type_encoder("null") - {:ok, "Encode.null"} - - iex> {:error, error} = determine_primitive_type_encoder("array") - iex> error.error_type - :unknown_primitive_type - - """ - @spec determine_primitive_type_encoder(String.t()) :: - {:ok, String.t()} | {:error, PrinterError.t()} - def determine_primitive_type_encoder(type_name) do - case type_name do - "string" -> - {:ok, "Encode.string"} - - "integer" -> - {:ok, "Encode.int"} - - "number" -> - {:ok, "Encode.float"} - - "boolean" -> - {:ok, "Encode.bool"} - - "null" -> - {:ok, "Encode.null"} - - _ -> - {:error, ErrorUtil.unknown_primitive_type(type_name)} - end - end - - # Printing encoders - end - - # Printing utils - start - - @spec qualify_name(SchemaDefinition.t(), String.t(), String.t()) :: String.t() - def qualify_name(schema_def, type_name, module_name) do - schema_name = schema_def.title - - if String.length(schema_name) > 0 do - "#{module_name}.#{schema_name}.#{type_name}" - else - "#{module_name}.#{type_name}" - end - end - - @doc ~S""" - Get the string shortname of the given struct. - - ## Examples - - iex> primitive_type = %JS2E.Types.PrimitiveType{name: "foo", - ...> path: ["#","foo"], - ...> type: "string"} - ...> get_string_name(primitive_type) - "PrimitiveType" - - """ - @spec get_string_name(struct) :: String.t() - def get_string_name(instance) when is_map(instance) do - instance.__struct__ - |> to_string - |> String.split(".") - |> List.last() - end - - # Printing utils - end - - # Predicate functions - start - - @spec primitive_type?(struct) :: boolean - def primitive_type?(type) do - get_string_name(type) == "PrimitiveType" - end - - @spec enum_type?(struct) :: boolean - def enum_type?(type) do - get_string_name(type) == "EnumType" - end - - @spec one_of_type?(struct) :: boolean - def one_of_type?(type) do - get_string_name(type) == "OneOfType" - end - - @spec union_type?(struct) :: boolean - def union_type?(type) do - get_string_name(type) == "UnionType" - end - - # Predicate functions - end - - @spec print_type( - Types.typeDefinition(), - SchemaDefinition.t(), - Types.schemaDictionary(), - String.t() - ) :: PrinterResult.t() - def print_type(type_def, schema_def, schema_dict, module_name) do - type_to_printer_dict = %{ - "AllOfType" => &AllOfPrinter.print_type/4, - "AnyOfType" => &AnyOfPrinter.print_type/4, - "ArrayType" => &ArrayPrinter.print_type/4, - "EnumType" => &EnumPrinter.print_type/4, - "ObjectType" => &ObjectPrinter.print_type/4, - "OneOfType" => &OneOfPrinter.print_type/4, - "PrimitiveType" => &PrimitivePrinter.print_type/4, - "TupleType" => &TuplePrinter.print_type/4, - "TypeReference" => &TypeReferencePrinter.print_type/4, - "UnionType" => &UnionPrinter.print_type/4 - } - - struct_name = get_string_name(type_def) - - if Map.has_key?(type_to_printer_dict, struct_name) do - type_printer = type_to_printer_dict[struct_name] - type_printer.(type_def, schema_def, schema_dict, module_name) - else - PrinterResult.new("", [ErrorUtil.unknown_type(struct_name)]) - end - end - - @spec print_decoder( - Types.typeDefinition(), - SchemaDefinition.t(), - Types.schemaDictionary(), - String.t() - ) :: PrinterResult.t() - def print_decoder(type_def, schema_def, schema_dict, module_name) do - type_to_printer_dict = %{ - "AllOfType" => &AllOfPrinter.print_decoder/4, - "AnyOfType" => &AnyOfPrinter.print_decoder/4, - "ArrayType" => &ArrayPrinter.print_decoder/4, - "EnumType" => &EnumPrinter.print_decoder/4, - "ObjectType" => &ObjectPrinter.print_decoder/4, - "OneOfType" => &OneOfPrinter.print_decoder/4, - "PrimitiveType" => &PrimitivePrinter.print_decoder/4, - "TupleType" => &TuplePrinter.print_decoder/4, - "TypeReference" => &TypeReferencePrinter.print_decoder/4, - "UnionType" => &UnionPrinter.print_decoder/4 - } - - struct_name = get_string_name(type_def) - - if Map.has_key?(type_to_printer_dict, struct_name) do - decoder_printer = type_to_printer_dict[struct_name] - decoder_printer.(type_def, schema_def, schema_dict, module_name) - else - PrinterResult.new("", [ErrorUtil.unknown_type(struct_name)]) - end - end - - @spec print_encoder( - Types.typeDefinition(), - SchemaDefinition.t(), - Types.schemaDictionary(), - String.t() - ) :: {:ok, String.t()} | {:error, PrinterError.t()} - def print_encoder(type_def, schema_def, schema_dict, module_name) do - type_to_printer_dict = %{ - "AllOfType" => &AllOfPrinter.print_encoder/4, - "AnyOfType" => &AnyOfPrinter.print_encoder/4, - "ArrayType" => &ArrayPrinter.print_encoder/4, - "EnumType" => &EnumPrinter.print_encoder/4, - "ObjectType" => &ObjectPrinter.print_encoder/4, - "OneOfType" => &OneOfPrinter.print_encoder/4, - "PrimitiveType" => &PrimitivePrinter.print_encoder/4, - "TupleType" => &TuplePrinter.print_encoder/4, - "TypeReference" => &TypeReferencePrinter.print_encoder/4, - "UnionType" => &UnionPrinter.print_encoder/4 - } - - struct_name = get_string_name(type_def) - - if Map.has_key?(type_to_printer_dict, struct_name) do - encoder_printer = type_to_printer_dict[struct_name] - encoder_printer.(type_def, schema_def, schema_dict, module_name) - else - PrinterResult.new("", [ErrorUtil.unknown_type(struct_name)]) - end - end - - @spec resolve_type( - TypePath.t(), - Types.typeIdentifier(), - SchemaDefinition.t(), - Types.schemaDictionary() - ) :: - {:ok, {Types.typeDefinition(), SchemaDefinition.t()}} - | {:error, PrinterError.t()} - def resolve_type(identifier, parent, schema_def, schema_dict) do - resolved_result = - cond do - identifier in ["string", "number", "integer", "boolean"] -> - resolve_primitive_identifier(identifier, schema_def) - - TypePath.type_path?(identifier) -> - resolve_type_path_identifier(identifier, parent, schema_def) - - URI.parse(identifier).scheme != nil -> - resolve_uri_identifier(identifier, parent, schema_dict) - - true -> - {:error, ErrorUtil.unresolved_reference(identifier, parent)} - end - - case resolved_result do - {:ok, {resolved_type, resolved_schema_def}} -> - if get_string_name(resolved_type) == "TypeReference" do - resolve_type( - resolved_type.path, - parent, - resolved_schema_def, - schema_dict - ) - else - {:ok, {resolved_type, resolved_schema_def}} - end - - {:error, error} -> - {:error, error} - end - end - - @spec resolve_primitive_identifier(String.t(), SchemaDefinition.t()) :: - {:ok, {Types.typeDefinition(), SchemaDefinition.t()}} - defp resolve_primitive_identifier(identifier, schema_def) do - primitive_type = PrimitiveType.new(identifier, identifier, identifier) - {:ok, {primitive_type, schema_def}} - end - - @spec resolve_type_path_identifier( - TypePath.t(), - TypePath.t(), - SchemaDefinition.t() - ) :: - {:ok, {Types.typeDefinition(), SchemaDefinition.t()}} - | {:error, PrinterError.t()} - defp resolve_type_path_identifier(identifier, parent, schema_def) do - type_dict = schema_def.types - resolved_type = type_dict[TypePath.to_string(identifier)] - - if resolved_type != nil do - {:ok, {resolved_type, schema_def}} - else - {:error, ErrorUtil.unresolved_reference(identifier, parent)} - end - end - - @spec resolve_uri_identifier( - TypePath.t(), - String.t(), - Types.schemaDictionary() - ) :: - {:ok, {Types.typeDefinition(), SchemaDefinition.t()}} - | {:error, PrinterError.t()} - defp resolve_uri_identifier(identifier, parent, schema_dict) do - schema_id = determine_schema_id(identifier) - schema_def = schema_dict[schema_id] - - if schema_def != nil do - type_dict = schema_def.types - - resolved_type = - if to_string(identifier) == schema_id do - type_dict["#"] - else - type_dict[to_string(identifier)] - end - - if resolved_type != nil do - {:ok, {resolved_type, schema_def}} - else - {:error, ErrorUtil.unresolved_reference(identifier, parent)} - end - else - {:error, ErrorUtil.unresolved_reference(identifier, parent)} - end - end - - @spec determine_schema_id(String.t()) :: String.t() - defp determine_schema_id(identifier) do - identifier - |> URI.parse() - |> Map.put(:fragment, nil) - |> to_string - end - - @spec split_ok_and_errors([{:ok, any} | {:error, PrinterError.t()}]) :: - {[any], [PrinterError.t()]} - def split_ok_and_errors(results) do - results - |> Enum.reverse() - |> Enum.reduce({[], []}, fn result, {oks, errors} -> - case result do - {:ok, ok} -> - {[ok | oks], errors} - - {:error, error} -> - {oks, [error | errors]} - end - end) - end -end diff --git a/lib/printer/utils/common_operations.ex b/lib/printer/utils/common_operations.ex new file mode 100644 index 0000000..ddc5244 --- /dev/null +++ b/lib/printer/utils/common_operations.ex @@ -0,0 +1,25 @@ +defmodule JS2E.Printer.Utils.CommonOperations do + @moduledoc ~S""" + Module containing various utility functions + for common operations across printers. + """ + + require Logger + alias JS2E.Printer.PrinterError + + @spec split_ok_and_errors([{:ok, any} | {:error, PrinterError.t()}]) :: + {[any], [PrinterError.t()]} + def split_ok_and_errors(results) do + results + |> Enum.reverse() + |> Enum.reduce({[], []}, fn result, {oks, errors} -> + case result do + {:ok, ok} -> + {[ok | oks], errors} + + {:error, error} -> + {oks, [error | errors]} + end + end) + end +end diff --git a/lib/printer/utils/elm_decoders.ex b/lib/printer/utils/elm_decoders.ex new file mode 100644 index 0000000..4f1bdd1 --- /dev/null +++ b/lib/printer/utils/elm_decoders.ex @@ -0,0 +1,102 @@ +defmodule JS2E.Printer.Utils.ElmDecoders do + @moduledoc ~S""" + Module containing common utility functions for outputting + Elm decoder definitions. + """ + + require Logger + alias JS2E.Types + alias JS2E.Types.{PrimitiveType, SchemaDefinition} + alias JS2E.Printer.Utils.Naming + alias JS2E.Printer.{ErrorUtil, PrinterError} + + @spec create_decoder_name( + {:ok, {Types.typeDefinition(), SchemaDefinition.t()}} + | {:error, PrinterError.t()}, + SchemaDefinition.t(), + String.t() + ) :: {:ok, String.t()} | {:error, PrinterError.t()} + def create_decoder_name({:error, error}, _schema, _name), do: {:error, error} + + def create_decoder_name( + {:ok, {resolved_type, resolved_schema}}, + context_schema, + module_name + ) do + decoder_name = + case resolved_type do + %PrimitiveType{} -> + determine_primitive_type_decoder(resolved_type.type) + + _ -> + type_name = + resolved_type.name |> Naming.normalize_identifier(:downcase) + + if type_name == "#" do + if resolved_schema.title != nil do + {:ok, "#{Naming.downcase_first(resolved_schema.title)}Decoder"} + else + {:ok, "rootDecoder"} + end + else + {:ok, "#{type_name}Decoder"} + end + end + + case decoder_name do + {:ok, decoder_name} -> + if resolved_schema.id != context_schema.id do + {:ok, Naming.qualify_name(resolved_schema, decoder_name, module_name)} + else + {:ok, decoder_name} + end + + {:error, error} -> + {:error, error} + end + end + + @doc ~S""" + Converts the following primitive types: "string", "integer", "number", + and "boolean" into their Elm decoder equivalent. Raises an error otherwise. + + ## Examples + + iex> determine_primitive_type_decoder("string") + {:ok, "Decode.string"} + + iex> determine_primitive_type_decoder("integer") + {:ok, "Decode.int"} + + iex> determine_primitive_type_decoder("number") + {:ok, "Decode.float"} + + iex> determine_primitive_type_decoder("boolean") + {:ok, "Decode.bool"} + + iex> {:error, error} = determine_primitive_type_decoder("array") + iex> error.error_type + :unknown_primitive_type + + """ + @spec determine_primitive_type_decoder(String.t()) :: + {:ok, String.t()} | {:error, PrinterError.t()} + def determine_primitive_type_decoder(type_name) do + case type_name do + "string" -> + {:ok, "Decode.string"} + + "integer" -> + {:ok, "Decode.int"} + + "number" -> + {:ok, "Decode.float"} + + "boolean" -> + {:ok, "Decode.bool"} + + _ -> + {:error, ErrorUtil.unknown_primitive_type(type_name)} + end + end +end diff --git a/lib/printer/utils/elm_encoders.ex b/lib/printer/utils/elm_encoders.ex new file mode 100644 index 0000000..e6ae2ce --- /dev/null +++ b/lib/printer/utils/elm_encoders.ex @@ -0,0 +1,111 @@ +defmodule JS2E.Printer.Utils.ElmEncoders do + @moduledoc ~S""" + Module containing common utility functions for outputting + Elm encoder definitions. + """ + + require Logger + alias JS2E.Types + alias JS2E.Types.{PrimitiveType, SchemaDefinition} + alias JS2E.Printer.Utils.Naming + alias JS2E.Printer.{ErrorUtil, PrinterError} + + @doc ~S""" + Returns the encoder name given a JSON schema type definition. + """ + @spec create_encoder_name( + {:ok, {Types.typeDefinition(), SchemaDefinition.t()}} + | {:error, PrinterError.t()}, + SchemaDefinition.t(), + String.t() + ) :: {:ok, String.t()} | {:error, PrinterError.t()} + def create_encoder_name({:error, error}, _schema, _name), do: {:error, error} + + def create_encoder_name( + {:ok, {resolved_type, resolved_schema}}, + context_schema, + module_name + ) do + encoder_name_result = + case resolved_type do + %PrimitiveType{} -> + determine_primitive_type_encoder(resolved_type.type) + + _ -> + type_name = resolved_type.name |> Naming.normalize_identifier(:upcase) + + if type_name == "#" do + if resolved_schema.title != nil do + {:ok, "encode#{Naming.upcase_first(resolved_schema.title)}"} + else + {:ok, "encodeRoot"} + end + else + {:ok, "encode#{Naming.upcase_first(type_name)}"} + end + end + + case encoder_name_result do + {:ok, encoder_name} -> + if resolved_schema.id != context_schema.id do + {:ok, Naming.qualify_name(resolved_schema, encoder_name, module_name)} + else + {:ok, encoder_name} + end + + {:error, error} -> + {:error, error} + end + end + + @doc ~S""" + Converts the following primitive types: "string", "integer", "number", + "boolean", and "null" into their Elm encoder equivalent. Raises an error + otherwise. + + ## Examples + + iex> determine_primitive_type_encoder("string") + {:ok, "Encode.string"} + + iex> determine_primitive_type_encoder("integer") + {:ok, "Encode.int"} + + iex> determine_primitive_type_encoder("number") + {:ok, "Encode.float"} + + iex> determine_primitive_type_encoder("boolean") + {:ok, "Encode.bool"} + + iex> determine_primitive_type_encoder("null") + {:ok, "Encode.null"} + + iex> {:error, error} = determine_primitive_type_encoder("array") + iex> error.error_type + :unknown_primitive_type + + """ + @spec determine_primitive_type_encoder(String.t()) :: + {:ok, String.t()} | {:error, PrinterError.t()} + def determine_primitive_type_encoder(type_name) do + case type_name do + "string" -> + {:ok, "Encode.string"} + + "integer" -> + {:ok, "Encode.int"} + + "number" -> + {:ok, "Encode.float"} + + "boolean" -> + {:ok, "Encode.bool"} + + "null" -> + {:ok, "Encode.null"} + + _ -> + {:error, ErrorUtil.unknown_primitive_type(type_name)} + end + end +end diff --git a/lib/printer/utils/elm_types.ex b/lib/printer/utils/elm_types.ex new file mode 100644 index 0000000..e8cc08d --- /dev/null +++ b/lib/printer/utils/elm_types.ex @@ -0,0 +1,92 @@ +defmodule JS2E.Printer.Utils.ElmTypes do + @moduledoc ~S""" + Module containing common utility functions for outputting Elm `type` + and `type alias` definitions. + """ + + require Logger + alias JS2E.Types + alias JS2E.Types.{PrimitiveType, SchemaDefinition} + alias JS2E.Printer.Utils.Naming + alias JS2E.Printer.{ErrorUtil, PrinterError} + + @spec create_type_name( + {:ok, {Types.typeDefinition(), SchemaDefinition.t()}} + | {:error, PrinterError.t()}, + SchemaDefinition.t(), + String.t() + ) :: {:ok, String.t()} | {:error, PrinterError.t()} + def create_type_name({:error, error}, _schema, _name), do: {:error, error} + + def create_type_name( + {:ok, {resolved_type, resolved_schema}}, + context_schema, + module_name + ) do + type_name = + case resolved_type do + %PrimitiveType{} -> + determine_primitive_type_name(resolved_type.type) + + _ -> + resolved_type_name = resolved_type.name + {:ok, Naming.upcase_first(resolved_type_name)} + end + + case type_name do + {:ok, type_name} -> + if resolved_schema.id != context_schema.id do + {:ok, Naming.qualify_name(resolved_schema, type_name, module_name)} + else + {:ok, type_name} + end + + {:error, error} -> + {:error, error} + end + end + + @doc ~S""" + Converts the following primitive types: "string", "integer", "number", + and "boolean" into their Elm type equivalent. Raises and error otherwise. + + ## Examples + + iex> determine_primitive_type_name("string") + {:ok, "String"} + + iex> determine_primitive_type_name("integer") + {:ok, "Int"} + + iex> determine_primitive_type_name("number") + {:ok, "Float"} + + iex> determine_primitive_type_name("boolean") + {:ok, "Bool"} + + iex> {:error, error} = determine_primitive_type_name("array") + iex> error.error_type + :unknown_primitive_type + + """ + @spec determine_primitive_type_name(String.t()) :: + {:ok, String.t()} | {:error, PrinterError.t()} + def determine_primitive_type_name(type_name) do + case type_name do + "string" -> + {:ok, "String"} + + "integer" -> + {:ok, "Int"} + + "number" -> + {:ok, "Float"} + + "boolean" -> + {:ok, "Bool"} + + _ -> + {:error, ErrorUtil.unknown_primitive_type(type_name)} + end + end +end diff --git a/lib/printer/utils/indentation.ex b/lib/printer/utils/indentation.ex new file mode 100644 index 0000000..9df794b --- /dev/null +++ b/lib/printer/utils/indentation.ex @@ -0,0 +1,30 @@ +defmodule JS2E.Printer.Utils.Indentation do + @moduledoc ~S""" + Module containing various utility functions for normalizing names of + identifiers in the Elm output. + """ + + @indent_size 4 + + @doc ~S""" + Returns a chunk of indentation. + + ## Examples + + iex> indent(2) + " " + + """ + @spec indent(pos_integer) :: String.t() + def indent(tabs \\ 1) when is_integer(tabs) do + String.pad_leading("", tabs * @indent_size) + end + + @doc ~S""" + Remove excessive newlines of a string. + """ + @spec trim_newlines(String.t()) :: String.t() + def trim_newlines(str) do + String.trim(str) <> "\n" + end +end diff --git a/lib/printer/utils/naming.ex b/lib/printer/utils/naming.ex new file mode 100644 index 0000000..504b249 --- /dev/null +++ b/lib/printer/utils/naming.ex @@ -0,0 +1,234 @@ +defmodule JS2E.Printer.Utils.Naming do + @moduledoc ~S""" + Module containing various utility functions for normalizing names of + identifiers in the Elm output. + """ + + alias JS2E.Types.SchemaDefinition + + @spec qualify_name(SchemaDefinition.t(), String.t(), String.t()) :: String.t() + def qualify_name(schema_def, type_name, module_name) do + schema_name = schema_def.title + + if String.length(schema_name) > 0 do + "#{module_name}.#{schema_name}.#{type_name}" + else + "#{module_name}.#{type_name}" + end + end + + @type casing :: :upcase | :downcase | :none + + @doc ~S""" + Normalizes a given identifier, i.e. translates numbers into plain + text, e.g. '0' becomes 'zero', and translates symbols into plain text, + e.g. '@' becomes 'at'. + + Also turns kebab-case, snake_case, and space case into camelCase. + + ## Examples + + iex> normalize_identifier("0") + "zero" + + iex> normalize_identifier("shape") + "shape" + + iex> normalize_identifier("myAngry!!Name") + "myAngryBangBangName" + + iex> normalize_identifier("name@Domain") + "nameAtDomain" + + iex> normalize_identifier("#Browns") + "hashBrowns" + + iex> normalize_identifier("$Bill") + "dollarBill" + + iex> normalize_identifier("identity") + "identity" + + iex> normalize_identifier("i want to be camel cased") + "iWantToBeCamelCased" + + iex> normalize_identifier("i-want-to-be-camel-cased") + "iWantToBeCamelCased" + + iex> normalize_identifier("DontEverChange") + "dontEverChange" + + iex> normalize_identifier("i_want_to_be_camel_cased") + "iWantToBeCamelCased" + + """ + @spec normalize_identifier(String.t(), casing) :: String.t() + def normalize_identifier(identifier, casing \\ :none) do + normalized_identifier = + identifier + |> kebab_to_camel_case + |> snake_to_camel_case + |> space_to_camel_case + |> normalize_name + |> normalize_symbols + |> downcase_first + + case casing do + :none -> + if determine_case(identifier) == :upcase do + upcase_first(normalized_identifier) + else + # We prefer to downcase + downcase_first(normalized_identifier) + end + + :upcase -> + upcase_first(normalized_identifier) + + :downcase -> + downcase_first(normalized_identifier) + end + end + + # Determines the casing of a string, e.g. + # - the string `"Abc"` returns `:upcase`, + # - the string `"abc"` returns `:downcase`, and + # - the string `"$bc"` returns `:none` + defp determine_case(s) do + first = s + + cond do + first |> String.upcase() == first and first |> String.downcase() != first -> + :upcase + + first |> String.upcase() != first and first |> String.downcase() == first -> + :downcase + + true -> + :none + end + end + + # Prettifies anonymous schema names like `0` and `1` into - slightly - + # better names like `zero` and `one` + @spec normalize_name(String.t()) :: String.t() + defp normalize_name("0"), do: "zero" + defp normalize_name("1"), do: "one" + defp normalize_name("2"), do: "two" + defp normalize_name("3"), do: "three" + defp normalize_name("4"), do: "four" + defp normalize_name("5"), do: "five" + defp normalize_name("6"), do: "six" + defp normalize_name("7"), do: "seven" + defp normalize_name("8"), do: "eight" + defp normalize_name("9"), do: "nine" + defp normalize_name("10"), do: "ten" + defp normalize_name(name), do: downcase_first(name) + + # Filters out or translates all symbols that the Elm compiler does not allow + # in an identifier: + + # ?!@#$%^&*()[]{}\/<>|`'",.+~=:; + + # into something more Elm parser friendly. Note that hyphens (-) and + # underscores (_) should be converted to camelCase using the appropriate + # helper functions. + @spec normalize_symbols(String.t()) :: String.t() + defp normalize_symbols(str) do + str + |> String.replace("?", "Huh") + |> String.replace("!", "Bang") + |> String.replace("@", "At") + |> String.replace("#", "Hash") + |> String.replace("$", "Dollar") + |> String.replace("%", "Percent") + |> String.replace("^", "Hat") + |> String.replace("&", "And") + |> String.replace("*", "Times") + |> String.replace("(", "LParen") + |> String.replace(")", "RParen") + |> String.replace("[", "LBracket") + |> String.replace("]", "RBracket") + |> String.replace("{", "LBrace") + |> String.replace("}", "RBrace") + |> String.replace("<", "Lt") + |> String.replace(">", "Gt") + |> String.replace("\\", "Backslash") + |> String.replace("/", "Slash") + |> String.replace("|", "Pipe") + |> String.replace("`", "Tick") + |> String.replace("'", "Quote") + |> String.replace("\"", "DoubleQuote") + |> String.replace(".", "Dot") + |> String.replace(",", "Comma") + |> String.replace("-", "Minus") + |> String.replace("+", "Plus") + |> String.replace("~", "Tilde") + |> String.replace("=", "Equal") + |> String.replace(":", "Colon") + |> String.replace(";", "Semicolon") + end + + # Turns a kebab-cased identifier into a camelCased one + @spec kebab_to_camel_case(String.t()) :: String.t() + defp kebab_to_camel_case(str) do + str + |> String.split("-") + |> Enum.map(fn word -> upcase_first(word) end) + |> Enum.join() + end + + # Turns a snake_cased identifier into a camelCased one + @spec snake_to_camel_case(String.t()) :: String.t() + defp snake_to_camel_case(str) do + str + |> String.split("_") + |> Enum.map(fn word -> upcase_first(word) end) + |> Enum.join() + end + + # Turns a space cased identifier into a camelCased one + @spec space_to_camel_case(String.t()) :: String.t() + defp space_to_camel_case(str) do + str + |> String.split(" ") + |> Enum.map(fn word -> upcase_first(word) end) + |> Enum.join() + end + + @doc ~S""" + Upcases the first letter of a string. + + ## Examples + + iex> upcase_first("foobar") + "Foobar" + + """ + @spec upcase_first(String.t()) :: String.t() + def upcase_first(string) when is_binary(string) do + if String.length(string) > 0 do + String.upcase(String.at(string, 0)) <> String.slice(string, 1..-1) + else + "" + end + end + + @doc ~S""" + Downcases the first letter of a string. + + ## Examples + + iex> downcase_first("Foobar") + "foobar" + + """ + @spec downcase_first(String.t()) :: String.t() + def downcase_first(string) when is_binary(string) do + if String.length(string) > 0 do + String.downcase(String.at(string, 0)) <> String.slice(string, 1..-1) + else + "" + end + end +end diff --git a/lib/printer/utils/resolve_type.ex b/lib/printer/utils/resolve_type.ex new file mode 100644 index 0000000..0337119 --- /dev/null +++ b/lib/printer/utils/resolve_type.ex @@ -0,0 +1,117 @@ +defmodule JS2E.Printer.Utils.ResolveType do + @moduledoc ~S""" + Module containing functions for resolving types. Main function being + the `resolve_type` function. + """ + + alias JS2E.{TypePath, Types} + alias JS2E.Types.{PrimitiveType, TypeReference, SchemaDefinition} + alias JS2E.Printer.{ErrorUtil, PrinterError} + + @spec resolve_type( + Types.typeIdentifier(), + Types.typeIdentifier(), + SchemaDefinition.t(), + Types.schemaDictionary() + ) :: + {:ok, {Types.typeDefinition(), SchemaDefinition.t()}} + | {:error, PrinterError.t()} + def resolve_type(identifier, parent, schema_def, schema_dict) do + resolved_result = + cond do + identifier in ["string", "number", "integer", "boolean"] -> + resolve_primitive_identifier(identifier, schema_def) + + TypePath.type_path?(identifier) -> + resolve_type_path_identifier(identifier, parent, schema_def) + + URI.parse(identifier).scheme != nil -> + resolve_uri_identifier(URI.parse(identifier), parent, schema_dict) + + true -> + {:error, ErrorUtil.unresolved_reference(identifier, parent)} + end + + case resolved_result do + {:ok, {resolved_type, resolved_schema_def}} -> + case resolved_type do + %TypeReference{} -> + resolve_type( + resolved_type.path, + parent, + resolved_schema_def, + schema_dict + ) + + _ -> + {:ok, {resolved_type, resolved_schema_def}} + end + + {:error, error} -> + {:error, error} + end + end + + @spec resolve_primitive_identifier(String.t(), SchemaDefinition.t()) :: + {:ok, {Types.typeDefinition(), SchemaDefinition.t()}} + defp resolve_primitive_identifier(identifier, schema_def) do + primitive_type = PrimitiveType.new(identifier, identifier, identifier) + {:ok, {primitive_type, schema_def}} + end + + @spec resolve_type_path_identifier( + TypePath.t(), + TypePath.t(), + SchemaDefinition.t() + ) :: + {:ok, {Types.typeDefinition(), SchemaDefinition.t()}} + | {:error, PrinterError.t()} + defp resolve_type_path_identifier(identifier, parent, schema_def) do + type_dict = schema_def.types + resolved_type = type_dict[TypePath.to_string(identifier)] + + if resolved_type != nil do + {:ok, {resolved_type, schema_def}} + else + {:error, ErrorUtil.unresolved_reference(identifier, parent)} + end + end + + @spec resolve_uri_identifier( + URI.t(), + Types.typeIdentifier(), + Types.schemaDictionary() + ) :: + {:ok, {Types.typeDefinition(), SchemaDefinition.t()}} + | {:error, PrinterError.t()} + defp resolve_uri_identifier(identifier, parent, schema_dict) do + schema_id = determine_schema_id(identifier) + schema_def = schema_dict[schema_id] + + if schema_def != nil do + type_dict = schema_def.types + + resolved_type = + if to_string(identifier) == schema_id do + type_dict["#"] + else + type_dict[to_string(identifier)] + end + + if resolved_type != nil do + {:ok, {resolved_type, schema_def}} + else + {:error, ErrorUtil.unresolved_reference(identifier, parent)} + end + else + {:error, ErrorUtil.unresolved_reference(identifier, parent)} + end + end + + @spec determine_schema_id(URI.t()) :: String.t() + defp determine_schema_id(identifier) do + identifier + |> Map.put(:fragment, nil) + |> to_string + end +end diff --git a/lib/types/all_of_type.ex b/lib/types/all_of_type.ex index f66f184..00dc817 100644 --- a/lib/types/all_of_type.ex +++ b/lib/types/all_of_type.ex @@ -4,61 +4,92 @@ defmodule JS2E.Types.AllOfType do JSON Schema: - "shape": { + The following example schema has the path "#/definitions/fancyCircle" + + { "allOf": [ { - "$ref": "#/definitions/circle" + "type": "object", + "properties": { + "color": { + "$ref": "#/definitions/color" + }, + "description": { + "type": "string" + } + }, + "required": [ "color" ] }, { - "$ref": "#/definitions/rectangle" + "$ref": "#/definitions/circle" } ] } + Where "#/definitions/color" resolves to: + + { + "type": "string", + "enum": ["red", "yellow", "green"] + } + + Where "#/definitions/circle" resolves to: + + { + "type": "object", + "properties": { + "radius": { + "type": "number" + } + }, + "required": [ "radius" ] + } + Elixir intermediate representation: - %AllOfType{name: "shape", - path: ["#", "shape"], - types: [["#", "shape", "allOf", "0"], - ["#", "shape", "allOf", "1"]]} + %AllOfType{name: "fancyCircle", + path: ["#", "definitions", "fancyCircle"], + types: [["#", "definitions", "fancyCircle", "allOf", "0"], + ["#", "definitions", "fancyCircle", "allOf", "1"]]} Elm code generated: - Type definition - type alias Shape = - { circle : Circle - , rectangle : Rectangle + type alias FancyCircle = + { zero : Zero + , circle : Circle } - Decoder definition - shapeDecoder : Decoder Shape - shapeDecoder = - decode Shape - |> required "circle" circleDecoder - |> required "rectangle" rectangleDecoder - - - Decoder usage - - |> required "shape" shapeDecoder + fancyCircleDecoder : Decoder FancyCircle + fancyCircleDecoder = + decode FancyCircle + |> custom zeroDecoder + |> custom circleDecoder - Encoder definition - encodeShape : Shape -> Value - encodeShape shape = + encodeFancyCircle : FancyCircle -> Value + encodeFancyCircle fancyCircle = let - circle = - encodeCircle shape.circle - - rectangle = - encodeRectangle shape.rectangle + color = + [ ( "color", encodeColor fancyCircle.zero.color ) ] + + description = + fancyCircle.zero.description + |> Maybe.map + (\description -> + [ ( "description", Encode.string description ) ] + ) + |> Maybe.withDefault [] + + radius = + [ ( "radius", Encode.float fancyCircle.circle.radius ) ] in - object <| circle ++ rectangle - - - Encoder usage - - encodeShape shape + object <| + color ++ description ++ radius """ diff --git a/lib/types/any_of_type.ex b/lib/types/any_of_type.ex index 1f07962..c8dc570 100644 --- a/lib/types/any_of_type.ex +++ b/lib/types/any_of_type.ex @@ -2,42 +2,70 @@ defmodule JS2E.Types.AnyOfType do @moduledoc ~S""" Represents a custom 'any_of' type definition in a JSON schema. - JSON Schema: + The following example schema has the path "#/definitions/fancyCircle" - "shape": { - "anyOf": [ + { + "allOf": [ { - "$ref": "#/definitions/circle" + "type": "object", + "properties": { + "color": { + "$ref": "#/definitions/color" + }, + "description": { + "type": "string" + } + }, + "required": [ "color" ] }, { - "$ref": "#/definitions/rectangle" + "$ref": "#/definitions/circle" } ] } + Where "#/definitions/color" resolves to: + + { + "type": "string", + "enum": ["red", "yellow", "green"] + } + + Where "#/definitions/circle" resolves to: + + { + "type": "object", + "properties": { + "radius": { + "type": "number" + } + }, + "required": [ "radius" ] + } + Elixir intermediate representation: - %AnyOfType{name: "shape", - path: ["#", "shape"], - types: [["#", "shape", "anyOf", "0"], - ["#", "shape", "anyOf", "1"]]} + %AnyOfType{name: "fancyCircle", + path: ["#", "definitions", "fancyCircle"], + types: [["#", "definitions", "fancyCircle", "allOf", "0"], + ["#", "definitions", "fancyCircle", "allOf", "1"]]} Elm code generated: - Type definition - type alias Shape = - { circle : Maybe Circle - , rectangle : Maybe Rectangle + type alias FancyCircle = + { zero : Maybe Zero + , circle : Maybe Circle } - Decoder definition - shapeDecoder : Decoder Shape - shapeDecoder = - decode Shape - |> optional "circle" (nullable circleDecoder) Nothing - |> optional "rectangle" (nullable rectangleDecoder) Nothing + fancyCircleDecoder : Decoder FancyCircle + fancyCircleDecoder = + decode FancyCircle + |> custom (nullable zeroDecoder) + |> custom (nullable circleDecoder) - Decoder usage @@ -45,20 +73,40 @@ defmodule JS2E.Types.AnyOfType do - Encoder definition - encodeShape : Shape -> Value - encodeShape shape = + encodeFancyCircle : FancyCircle -> Value + encodeFancyCircle fancyCircle = let - circle = - encodeCircle shape.circle - - rectangle = - encodeRectangle shape.rectangle + color = + fancyCircle.zero + |> Maybe.map + (\zero -> + [ ( "color", encodeColor zero.color ) ] + ) + |> Maybe.withDefault [] + + description = + fancyCircle.zero + |> Maybe.map + (\zero -> + zero.description + |> Maybe.map + (\description -> + [ ( "description", Encode.string description ) ] + ) + |> Maybe.withDefault [] + ) + |> Maybe.withDefault [] + + radius = + fancyCircle.circle + |> Maybe.map + (\circle -> + [ ( "radius", Encode.float circle.radius ) ] + ) + |> Maybe.withDefault [] in - object <| circle ++ rectangle - - - Encoder usage - - encodeShape shape + object <| + color ++ description ++ radius """ diff --git a/lib/types/type_reference.ex b/lib/types/type_reference.ex index 6d62ced..c60d72a 100644 --- a/lib/types/type_reference.ex +++ b/lib/types/type_reference.ex @@ -32,7 +32,7 @@ defmodule JS2E.Types.TypeReference do Elixir intermediate representation: %TypeReference{name: "self", - path: ["#", "definitions", "link"]} + path: ["#", "definitions", "foo"]} %TypeReference{name: "other", path: %URI{scheme: "http", diff --git a/priv/templates/all_of/decoder.elm.eex b/priv/templates/all_of/decoder.elm.eex index c6d7922..401f8e6 100644 --- a/priv/templates/all_of/decoder.elm.eex +++ b/priv/templates/all_of/decoder.elm.eex @@ -3,8 +3,8 @@ decode <%= type_name %><%# %><%= for clause <- clauses do %><%# %><%= if Map.has_key?(clause, :property_decoder) do %> - |> required "<%= clause.property_name %>" (<%= clause.property_decoder %> |> andThen <%= clause.decoder %>)<%# + |> custom (<%= clause.property_decoder %> |> andThen <%= clause.decoder %>)<%# %><% else %> - |> required "<%= clause.property_name %>" <%= clause.decoder_name %><%# + |> custom <%= clause.decoder_name %><%# %><% end %><%# %><% end %> diff --git a/priv/templates/all_of/encoder.elm.eex b/priv/templates/all_of/encoder.elm.eex index 5e29810..22efaf8 100644 --- a/priv/templates/all_of/encoder.elm.eex +++ b/priv/templates/all_of/encoder.elm.eex @@ -1,8 +1,14 @@ <%= encoder_name %> : <%= type_name %> -> Value <%= encoder_name %> <%= argument_name %> = let<%= for property <- properties do %> - <%= property.name %> = - <%= property.encoder_name %> <%= argument_name %>.<%= property.name %> + <%= property.name %> =<%= if property.required == true do %> + [ ( "<%= property.name %>", <%= property.encoder_name %> <%= argument_name %>.<%= property.parent_name %>.<%= property.name %> ) ]<% else %> + <%= argument_name %>.<%= property.parent_name %>.<%= property.name %> + |> Maybe.map + (\<%= property.name %> -> + [ ( "<%= property.name %>", <%= property.encoder_name %> <%= property.name %> ) ] + ) + |> Maybe.withDefault []<% end %> <% end %><%# %> in object <| diff --git a/priv/templates/any_of/decoder.elm.eex b/priv/templates/any_of/decoder.elm.eex index 0bf02c4..cb14a37 100644 --- a/priv/templates/any_of/decoder.elm.eex +++ b/priv/templates/any_of/decoder.elm.eex @@ -3,8 +3,8 @@ decode <%= type_name %><%# %><%= for clause <- clauses do %><%# %><%= if Map.has_key?(clause, :property_decoder) do %> - |> optional "<%= clause.property_name %>" (<%= clause.property_decoder %> |> andThen <%= clause.decoder %> |> maybe) Nothing<%# + |> custom (nullable (<%= clause.property_decoder %> |> andThen <%= clause.decoder %>))<%# %><% else %> - |> optional "<%= clause.property_name %>" (nullable <%= clause.decoder_name %>) Nothing<%# + |> custom (nullable <%= clause.decoder_name %>)<%# %><% end %><%# %><% end %> diff --git a/priv/templates/any_of/encoder.elm.eex b/priv/templates/any_of/encoder.elm.eex index dcafaba..0497750 100644 --- a/priv/templates/any_of/encoder.elm.eex +++ b/priv/templates/any_of/encoder.elm.eex @@ -2,12 +2,18 @@ <%= encoder_name %> <%= argument_name %> = let<%= for property <- properties do %> <%= property.name %> = - case <%= argument_name %>.<%= property.name %> of - Just <%= property.name %> -> - <%= property.encoder_name %> <%= property.name %> - - Nothing -> - [] + <%= argument_name %>.<%= property.parent_name %> + |> Maybe.map + (\<%= property.parent_name %> -><%= if property.required == true do %> + [ ( "<%= property.name %>", <%= property.encoder_name %> <%= property.parent_name %>.<%= property.name %> ) ]<% else %> + <%= property.parent_name %>.<%= property.name %> + |> Maybe.map + (\<%= property.name %> -> + [ ( "<%= property.name %>", <%= property.encoder_name %> <%= property.name %> ) ] + ) + |> Maybe.withDefault []<% end %> + ) + |> Maybe.withDefault [] <% end %><%# %> in object <| diff --git a/priv/templates/preamble/preamble.elm.eex b/priv/templates/preamble/preamble.elm.eex index 38c9a86..802d5c1 100644 --- a/priv/templates/preamble/preamble.elm.eex +++ b/priv/templates/preamble/preamble.elm.eex @@ -30,5 +30,5 @@ import Json.Encode as Encode , list ) <%= for import_name <- imports do %><%# -%>import <%= prefix %><%= import_name %><%# -%><% end %> +%>import <%= prefix %><%= import_name %> +<% end %> diff --git a/test/parser/all_of_parser_test.exs b/test/parser/all_of_parser_test.exs index 00a8989..9ea79d0 100644 --- a/test/parser/all_of_parser_test.exs +++ b/test/parser/all_of_parser_test.exs @@ -6,85 +6,87 @@ defmodule JS2ETest.Parser.AllOfParser do alias JS2E.Types.{AllOfType, ObjectType, PrimitiveType, TypeReference} alias JS2E.Parser.AllOfParser - test "parse primitive all_of type" do - parser_result = - ~S""" - { - "allOf": [ - { - "type": "object", - "properties": { - "color": { - "$ref": "#/color" - }, - "title": { - "type": "string" - }, - "radius": { - "type": "number" - } + defp all_of_type do + ~S""" + { + "allOf": [ + { + "type": "object", + "properties": { + "color": { + "$ref": "#/definitions/color" }, - "required": [ "color", "radius" ] + "description": { + "type": "string" + } }, - { - "type": "string" - } - ] - } - """ + "required": [ "color" ] + }, + { + "$ref": "#/definitions/circle" + } + ] + } + """ + end + + defp parent_id, do: "http://www.example.com/schemas/fancyCircle.json" + defp id, do: nil + defp path, do: ["#", "definitions", "fancyCircle"] + defp name, do: "fancyCircle" + + test "can parse all_of type" do + parser_result = + all_of_type() |> Poison.decode!() - |> AllOfParser.parse(nil, nil, ["#", "schema"], "schema") + |> AllOfParser.parse(parent_id(), id(), path(), name()) + + expected_all_of_type = %AllOfType{ + name: "fancyCircle", + path: ["#", "definitions", "fancyCircle"], + types: [ + path() ++ ["allOf", "0"], + path() ++ ["allOf", "1"] + ] + } expected_object_type = %ObjectType{ name: "0", - path: ["#", "schema", "allOf", "0"], - required: ["color", "radius"], + path: path() ++ ["allOf", "0"], + required: ["color"], properties: %{ - "color" => ["#", "schema", "allOf", "0", "properties", "color"], - "title" => ["#", "schema", "allOf", "0", "properties", "title"], - "radius" => ["#", "schema", "allOf", "0", "properties", "radius"] + "color" => path() ++ ["allOf", "0", "properties", "color"], + "description" => path() ++ ["allOf", "0", "properties", "description"] } } - expected_primitive_type = %PrimitiveType{ - name: "1", - path: ["#", "schema", "allOf", "1"], - type: "string" + expected_color_type = %TypeReference{ + name: "color", + path: ["#", "definitions", "color"] } - expected_color_type = %TypeReference{name: "color", path: ["#", "color"]} - - expected_radius_type = %PrimitiveType{ - name: "radius", - path: ["#", "schema", "allOf", "0", "properties", "radius"], - type: "number" - } - - expected_title_type = %PrimitiveType{ - name: "title", - path: ["#", "schema", "allOf", "0", "properties", "title"], + expected_description_type = %PrimitiveType{ + name: "description", + path: path() ++ ["allOf", "0", "properties", "description"], type: "string" } - expected_all_of_type = %AllOfType{ - name: "schema", - path: ["#", "schema"], - types: [ - ["#", "schema", "allOf", "0"], - ["#", "schema", "allOf", "1"] - ] + expected_circle_type = %TypeReference{ + name: "1", + path: ["#", "definitions", "circle"] } assert parser_result.errors == [] assert parser_result.warnings == [] assert parser_result.type_dict == %{ - "#/schema" => expected_all_of_type, - "#/schema/allOf/0" => expected_object_type, - "#/schema/allOf/1" => expected_primitive_type, - "#/schema/allOf/0/properties/color" => expected_color_type, - "#/schema/allOf/0/properties/radius" => expected_radius_type, - "#/schema/allOf/0/properties/title" => expected_title_type + "#/definitions/fancyCircle" => expected_all_of_type, + "#/definitions/fancyCircle/allOf/0" => expected_object_type, + "#/definitions/fancyCircle/allOf/0/properties/color" => + expected_color_type, + "#/definitions/fancyCircle/allOf/0/properties/description" => + expected_description_type, + "#/definitions/fancyCircle/allOf/1" => expected_circle_type } end end diff --git a/test/parser/definitions_parser_test.exs b/test/parser/definitions_parser_test.exs index d69a213..8a1cc2b 100644 --- a/test/parser/definitions_parser_test.exs +++ b/test/parser/definitions_parser_test.exs @@ -11,7 +11,6 @@ defmodule JS2ETest.Parser.DefinitionsParser do ~S""" { "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Root", "id": "http://example.com/root.json", "type": "array", "items": { "$ref": "#/definitions/positiveInteger" }, @@ -28,7 +27,7 @@ defmodule JS2ETest.Parser.DefinitionsParser do |> RootParser.parse_schema("examples/example.json") expected_root_type_reference = %ArrayType{ - name: "#", + name: "Root", path: ["#"], items: ["#", "items"] } diff --git a/test/parser/internal_references_test.exs b/test/parser/internal_references_test.exs index 9766c2e..3633589 100644 --- a/test/parser/internal_references_test.exs +++ b/test/parser/internal_references_test.exs @@ -11,9 +11,8 @@ defmodule JS2ETest.Parser.InternalReferences do { "$schema": "http://json-schema.org/draft-04/schema#", "description": "Demonstrates the different types of internal references", - "title": "Internal references", + "title": "InternalReference", "id": "http://example.com/root.json", - "type": "object", "$ref": "#/definitions/C", "definitions": { "A": { @@ -44,7 +43,7 @@ defmodule JS2ETest.Parser.InternalReferences do |> RootParser.parse_schema("examples/example.json") expected_root_type_reference = %TypeReference{ - name: "#", + name: "InternalReference", path: ["#", "definitions", "C"] } @@ -80,7 +79,7 @@ defmodule JS2ETest.Parser.InternalReferences do file_path: "examples/example.json", description: "Demonstrates the different types of internal references", - title: "Internal references", + title: "InternalReference", id: URI.parse("http://example.com/root.json"), types: %{ "#" => expected_root_type_reference, diff --git a/test/printer/all_of_printer_test.exs b/test/printer/all_of_printer_test.exs index 86b0141..0d00521 100644 --- a/test/printer/all_of_printer_test.exs +++ b/test/printer/all_of_printer_test.exs @@ -2,61 +2,99 @@ defmodule JS2ETest.Printer.AllOfPrinter do use ExUnit.Case require Logger - alias JS2E.Types.{AllOfType, ObjectType, TypeReference, SchemaDefinition} + + alias JS2E.Types.{ + AllOfType, + EnumType, + ObjectType, + PrimitiveType, + TypeReference, + SchemaDefinition + } + alias JS2E.Printer.AllOfPrinter - test "print 'all of' type value" do - module_name = "Domain" + defp path, do: ["#", "definitions", "fancyCircle"] - type_dict = %{ - "#/shape/0" => %TypeReference{ - name: "square", - path: ["#", "definitions", "square"] - }, - "#/shape/1" => %TypeReference{ - name: "circle", - path: ["#", "definitions", "circle"] - }, - "#/definitions/square" => %ObjectType{ - name: "square", - path: ["#"], - required: ["color", "size"], + def all_of_type do + %AllOfType{ + name: "fancyCircle", + path: ["#", "definitions", "fancyCircle"], + types: [ + path() ++ ["allOf", "0"], + path() ++ ["allOf", "1"] + ] + } + end + + def schema_def do + %SchemaDefinition{ + description: "'allOf' example schema", + id: URI.parse("http://example.com/all_of_example.json"), + title: "AllOfExample", + types: type_dict() + } + end + + def type_dict do + %{ + "#/definitions/fancyCircle/allOf/0" => %ObjectType{ + name: "0", + path: path() ++ ["allOf", "0"], + required: ["color"], properties: %{ - "color" => ["#", "properties", "color"], - "title" => ["#", "properties", "size"] + "color" => path() ++ ["allOf", "0", "properties", "color"], + "description" => path() ++ ["allOf", "0", "properties", "description"] } }, + "#/definitions/fancyCircle/allOf/0/properties/color" => %TypeReference{ + name: "color", + path: ["#", "definitions", "color"] + }, + "#/definitions/color" => %EnumType{ + name: "color", + path: ["#", "definitions", "color"], + type: "string", + values: ["red", "yellow", "green"] + }, + "#/definitions/fancyCircle/allOf/0/properties/description" => + %PrimitiveType{ + name: "description", + path: path() ++ ["allOf", "0", "properties", "description"], + type: "string" + }, + "#/definitions/fancyCircle/allOf/1" => %TypeReference{ + name: "1", + path: ["#", "definitions", "circle"] + }, "#/definitions/circle" => %ObjectType{ name: "circle", - path: ["#"], - required: ["color", "radius"], + path: ["#", "definitions", "circle"], + required: ["radius"], properties: %{ - "color" => ["#", "properties", "color"], - "radius" => ["#", "properties", "radius"] + "radius" => ["#", "definitions", "circle", "properties", "radius"] } + }, + "#/definitions/circle/properties/radius" => %PrimitiveType{ + name: "radius", + path: ["#", "definitions", "circle", "properties", "radius"], + type: "number" } } + end - schema_def = %SchemaDefinition{ - description: "Test schema", - id: URI.parse("http://example.com/test.json"), - title: "Test", - types: type_dict - } + def module_name, do: "Data" + test "print 'all of' type value" do result = - %AllOfType{ - name: "shape", - path: ["#", "definitions", "shape"], - types: [["#", "shape", "0"], ["#", "shape", "1"]] - } - |> AllOfPrinter.print_type(schema_def, %{}, module_name) + all_of_type() + |> AllOfPrinter.print_type(schema_def(), %{}, module_name()) all_of_type_program = result.printed_schema expected_all_of_type_program = """ - type alias Shape = - { square : Square + type alias FancyCircle = + { zero : Zero , circle : Circle } """ @@ -65,50 +103,16 @@ defmodule JS2ETest.Printer.AllOfPrinter do end test "print 'all of' decoder" do - module_name = "Domain" - - type_dict = %{ - "#/definitions/square" => %ObjectType{ - name: "square", - path: ["#"], - required: ["color", "size"], - properties: %{ - "color" => ["#", "properties", "color"], - "title" => ["#", "properties", "size"] - } - }, - "#/definitions/circle" => %ObjectType{ - name: "circle", - path: ["#"], - required: ["color", "radius"], - properties: %{ - "color" => ["#", "properties", "color"], - "radius" => ["#", "properties", "radius"] - } - } - } - - schema_def = %SchemaDefinition{ - description: "Test schema", - id: URI.parse("http://example.com/test.json"), - title: "Test", - types: type_dict - } - result = - %AllOfType{ - name: "shape", - path: ["#", "definitions", "shape"], - types: [["#", "definitions", "square"], ["#", "definitions", "circle"]] - } - |> AllOfPrinter.print_decoder(schema_def, %{}, module_name) + all_of_type() + |> AllOfPrinter.print_decoder(schema_def(), %{}, module_name()) expected_all_of_decoder_program = """ - shapeDecoder : Decoder Shape - shapeDecoder = - decode Shape - |> required "square" squareDecoder - |> required "circle" circleDecoder + fancyCircleDecoder : Decoder FancyCircle + fancyCircleDecoder = + decode FancyCircle + |> custom zeroDecoder + |> custom circleDecoder """ all_of_decoder_program = result.printed_schema @@ -117,56 +121,30 @@ defmodule JS2ETest.Printer.AllOfPrinter do end test "print 'all of' encoder" do - module_name = "Domain" - - type_dict = %{ - "#/definitions/square" => %ObjectType{ - name: "square", - path: ["#"], - required: ["color", "size"], - properties: %{ - "color" => ["#", "properties", "color"], - "title" => ["#", "properties", "size"] - } - }, - "#/definitions/circle" => %ObjectType{ - name: "circle", - path: ["#"], - required: ["color", "radius"], - properties: %{ - "color" => ["#", "properties", "color"], - "radius" => ["#", "properties", "radius"] - } - } - } - - schema_def = %SchemaDefinition{ - description: "Test schema", - id: URI.parse("http://example.com/test.json"), - title: "Test", - types: type_dict - } - result = - %AllOfType{ - name: "shape", - path: ["#", "definitions", "shape"], - types: [["#", "definitions", "square"], ["#", "definitions", "circle"]] - } - |> AllOfPrinter.print_encoder(schema_def, %{}, module_name) + all_of_type() + |> AllOfPrinter.print_encoder(schema_def(), %{}, module_name()) expected_all_of_encoder_program = """ - encodeShape : Shape -> Value - encodeShape shape = + encodeFancyCircle : FancyCircle -> Value + encodeFancyCircle fancyCircle = let - square = - encodeSquare shape.square - - circle = - encodeCircle shape.circle + color = + [ ( "color", encodeColor fancyCircle.zero.color ) ] + + description = + fancyCircle.zero.description + |> Maybe.map + (\\description -> + [ ( "description", Encode.string description ) ] + ) + |> Maybe.withDefault [] + + radius = + [ ( "radius", Encode.float fancyCircle.circle.radius ) ] in object <| - square ++ circle + color ++ description ++ radius """ all_of_encoder_program = result.printed_schema diff --git a/test/printer/any_of_printer_test.exs b/test/printer/any_of_printer_test.exs index e98b54a..e7aa656 100644 --- a/test/printer/any_of_printer_test.exs +++ b/test/printer/any_of_printer_test.exs @@ -2,113 +2,117 @@ defmodule JS2ETest.Printer.AnyOfPrinter do use ExUnit.Case require Logger - alias JS2E.Types.{AnyOfType, ObjectType, TypeReference, SchemaDefinition} + + alias JS2E.Types.{ + AnyOfType, + EnumType, + ObjectType, + PrimitiveType, + TypeReference, + SchemaDefinition + } + alias JS2E.Printer.AnyOfPrinter - test "print 'any of' type value" do - module_name = "Domain" + defp path, do: ["#", "definitions", "fancyCircle"] - type_dict = %{ - "#/shape/0" => %TypeReference{ - name: "square", - path: ["#", "definitions", "square"] - }, - "#/shape/1" => %TypeReference{ - name: "circle", - path: ["#", "definitions", "circle"] - }, - "#/definitions/square" => %ObjectType{ - name: "square", - path: ["#"], - required: ["color", "size"], + def any_of_type do + %AnyOfType{ + name: "fancyCircle", + path: ["#", "definitions", "fancyCircle"], + types: [ + path() ++ ["anyOf", "0"], + path() ++ ["anyOf", "1"] + ] + } + end + + def schema_def do + %SchemaDefinition{ + description: "'anyOf' example schema", + id: URI.parse("http://example.com/any_of_example.json"), + title: "AnyOfExample", + types: type_dict() + } + end + + def type_dict do + %{ + "#/definitions/fancyCircle/anyOf/0" => %ObjectType{ + name: "0", + path: path() ++ ["anyOf", "0"], + required: ["color"], properties: %{ - "color" => ["#", "properties", "color"], - "title" => ["#", "properties", "size"] + "color" => path() ++ ["anyOf", "0", "properties", "color"], + "description" => path() ++ ["anyOf", "0", "properties", "description"] } }, + "#/definitions/fancyCircle/anyOf/0/properties/color" => %TypeReference{ + name: "color", + path: ["#", "definitions", "color"] + }, + "#/definitions/color" => %EnumType{ + name: "color", + path: ["#", "definitions", "color"], + type: "string", + values: ["red", "yellow", "green"] + }, + "#/definitions/fancyCircle/anyOf/0/properties/description" => + %PrimitiveType{ + name: "description", + path: path() ++ ["anyOf", "0", "properties", "description"], + type: "string" + }, + "#/definitions/fancyCircle/anyOf/1" => %TypeReference{ + name: "1", + path: ["#", "definitions", "circle"] + }, "#/definitions/circle" => %ObjectType{ name: "circle", - path: ["#"], - required: ["color", "radius"], + path: ["#", "definitions", "circle"], + required: ["radius"], properties: %{ - "color" => ["#", "properties", "color"], - "radius" => ["#", "properties", "radius"] + "radius" => ["#", "definitions", "circle", "properties", "radius"] } + }, + "#/definitions/circle/properties/radius" => %PrimitiveType{ + name: "radius", + path: ["#", "definitions", "circle", "properties", "radius"], + type: "number" } } + end - schema_def = %SchemaDefinition{ - description: "Test schema", - id: URI.parse("http://example.com/test.json"), - title: "Test", - types: type_dict - } + def module_name, do: "Data" + test "print 'any of' type value" do result = - %AnyOfType{ - name: "shape", - path: ["#", "definitions", "shape"], - types: [["#", "shape", "0"], ["#", "shape", "1"]] - } - |> AnyOfPrinter.print_type(schema_def, %{}, module_name) + any_of_type() + |> AnyOfPrinter.print_type(schema_def(), %{}, module_name()) + + any_of_type_program = result.printed_schema expected_any_of_type_program = """ - type alias Shape = - { square : Maybe Square + type alias FancyCircle = + { zero : Maybe Zero , circle : Maybe Circle } """ - any_of_type_program = result.printed_schema - assert any_of_type_program == expected_any_of_type_program end test "print 'any of' decoder" do - module_name = "Domain" - - type_dict = %{ - "#/definitions/square" => %ObjectType{ - name: "square", - path: ["#"], - required: ["color", "size"], - properties: %{ - "color" => ["#", "properties", "color"], - "title" => ["#", "properties", "size"] - } - }, - "#/definitions/circle" => %ObjectType{ - name: "circle", - path: ["#"], - required: ["color", "radius"], - properties: %{ - "color" => ["#", "properties", "color"], - "radius" => ["#", "properties", "radius"] - } - } - } - - schema_def = %SchemaDefinition{ - description: "Test schema", - id: URI.parse("http://example.com/test.json"), - title: "Test", - types: type_dict - } - result = - %AnyOfType{ - name: "shape", - path: ["#", "definitions", "shape"], - types: [["#", "definitions", "square"], ["#", "definitions", "circle"]] - } - |> AnyOfPrinter.print_decoder(schema_def, %{}, module_name) + any_of_type() + |> AnyOfPrinter.print_decoder(schema_def(), %{}, module_name()) expected_any_of_decoder_program = """ - shapeDecoder : Decoder Shape - shapeDecoder = - decode Shape - |> optional "square" (nullable squareDecoder) Nothing - |> optional "circle" (nullable circleDecoder) Nothing + fancyCircleDecoder : Decoder FancyCircle + fancyCircleDecoder = + decode FancyCircle + |> custom (nullable zeroDecoder) + |> custom (nullable circleDecoder) """ any_of_decoder_program = result.printed_schema @@ -117,70 +121,49 @@ defmodule JS2ETest.Printer.AnyOfPrinter do end test "print 'any of' encoder" do - module_name = "Domain" - - type_dict = %{ - "#/definitions/square" => %ObjectType{ - name: "square", - path: ["#"], - required: ["color", "size"], - properties: %{ - "color" => ["#", "properties", "color"], - "title" => ["#", "properties", "size"] - } - }, - "#/definitions/circle" => %ObjectType{ - name: "circle", - path: ["#"], - required: ["color", "radius"], - properties: %{ - "color" => ["#", "properties", "color"], - "radius" => ["#", "properties", "radius"] - } - } - } - - schema_def = %SchemaDefinition{ - description: "Test schema", - id: URI.parse("http://example.com/test.json"), - title: "Test", - types: type_dict - } - result = - %AnyOfType{ - name: "shape", - path: ["#", "definitions", "shape"], - types: [["#", "definitions", "square"], ["#", "definitions", "circle"]] - } - |> AnyOfPrinter.print_encoder(schema_def, %{}, module_name) + any_of_type() + |> AnyOfPrinter.print_encoder(schema_def(), %{}, module_name()) + + any_of_encoder_program = result.printed_schema expected_any_of_encoder_program = """ - encodeShape : Shape -> Value - encodeShape shape = + encodeFancyCircle : FancyCircle -> Value + encodeFancyCircle fancyCircle = let - square = - case shape.square of - Just square -> - encodeSquare square - - Nothing -> - [] - - circle = - case shape.circle of - Just circle -> - encodeCircle circle - - Nothing -> - [] + color = + fancyCircle.zero + |> Maybe.map + (\\zero -> + [ ( "color", encodeColor zero.color ) ] + ) + |> Maybe.withDefault [] + + description = + fancyCircle.zero + |> Maybe.map + (\\zero -> + zero.description + |> Maybe.map + (\\description -> + [ ( "description", Encode.string description ) ] + ) + |> Maybe.withDefault [] + ) + |> Maybe.withDefault [] + + radius = + fancyCircle.circle + |> Maybe.map + (\\circle -> + [ ( "radius", Encode.float circle.radius ) ] + ) + |> Maybe.withDefault [] in object <| - square ++ circle + color ++ description ++ radius """ - any_of_encoder_program = result.printed_schema - assert any_of_encoder_program == expected_any_of_encoder_program end end diff --git a/test/printer/util_test.exs b/test/printer/util_test.exs deleted file mode 100644 index 715bc96..0000000 --- a/test/printer/util_test.exs +++ /dev/null @@ -1,4 +0,0 @@ -defmodule JS2ETest.Printer.Util do - use ExUnit.Case, async: true - doctest JS2E.Printer.Util, import: true -end diff --git a/test/printer/utils/common_operations_test.exs b/test/printer/utils/common_operations_test.exs new file mode 100644 index 0000000..65db6ee --- /dev/null +++ b/test/printer/utils/common_operations_test.exs @@ -0,0 +1,4 @@ +defmodule JS2ETest.Printer.Utils.CommonOperations do + use ExUnit.Case, async: true + doctest JS2E.Printer.Utils.CommonOperations, import: true +end diff --git a/test/printer/utils/elm_decoders_test.exs b/test/printer/utils/elm_decoders_test.exs new file mode 100644 index 0000000..3d3e87c --- /dev/null +++ b/test/printer/utils/elm_decoders_test.exs @@ -0,0 +1,4 @@ +defmodule JS2ETest.Printer.Utils.ElmDecoders do + use ExUnit.Case, async: true + doctest JS2E.Printer.Utils.ElmDecoders, import: true +end diff --git a/test/printer/utils/elm_encoders_test.exs b/test/printer/utils/elm_encoders_test.exs new file mode 100644 index 0000000..ad302a5 --- /dev/null +++ b/test/printer/utils/elm_encoders_test.exs @@ -0,0 +1,4 @@ +defmodule JS2ETest.Printer.Utils.ElmEncoders do + use ExUnit.Case, async: true + doctest JS2E.Printer.Utils.ElmEncoders, import: true +end diff --git a/test/printer/utils/elm_types_test.exs b/test/printer/utils/elm_types_test.exs new file mode 100644 index 0000000..0e4bb7d --- /dev/null +++ b/test/printer/utils/elm_types_test.exs @@ -0,0 +1,4 @@ +defmodule JS2ETest.Printer.Utils.ElmTypes do + use ExUnit.Case, async: true + doctest JS2E.Printer.Utils.ElmTypes, import: true +end diff --git a/test/printer/utils/indentation_test.exs b/test/printer/utils/indentation_test.exs new file mode 100644 index 0000000..440e918 --- /dev/null +++ b/test/printer/utils/indentation_test.exs @@ -0,0 +1,4 @@ +defmodule JS2ETest.Printer.Utils.Indentation do + use ExUnit.Case, async: true + doctest JS2E.Printer.Utils.Indentation, import: true +end diff --git a/test/printer/utils/naming_test.exs b/test/printer/utils/naming_test.exs new file mode 100644 index 0000000..f3a6e55 --- /dev/null +++ b/test/printer/utils/naming_test.exs @@ -0,0 +1,4 @@ +defmodule JS2ETest.Printer.Utils.Naming do + use ExUnit.Case, async: true + doctest JS2E.Printer.Utils.Naming, import: true +end diff --git a/test/printer/utils/resolve_type_test.exs b/test/printer/utils/resolve_type_test.exs new file mode 100644 index 0000000..d72eba7 --- /dev/null +++ b/test/printer/utils/resolve_type_test.exs @@ -0,0 +1,4 @@ +defmodule JS2ETest.Printer.Utils.ResolveType do + use ExUnit.Case, async: true + doctest JS2E.Printer.Utils.ResolveType, import: true +end