├── .formatter.exs ├── .github ├── dependabot.yml └── workflows │ └── elixir.yml ├── .gitignore ├── .tool-versions ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── config ├── config.exs ├── dev.exs ├── prod.exs └── test.exs ├── coveralls.json ├── examples ├── example-input-json-schemas │ ├── circle.json │ └── definitions.json └── example-output-elm-code │ ├── .tool-versions │ ├── elm.json │ ├── package.json │ ├── src │ ├── Data │ │ ├── Circle.elm │ │ └── Definitions.elm │ └── Helper │ │ └── Encode.elm │ └── tests │ └── Data │ ├── CircleTests.elm │ └── DefinitionsTests.elm ├── lib ├── js2e.ex └── printer │ ├── all_of_printer.ex │ ├── any_of_printer.ex │ ├── array_printer.ex │ ├── enum_printer.ex │ ├── error_util.ex │ ├── object_printer.ex │ ├── one_of_printer.ex │ ├── preamble_printer.ex │ ├── primitive_printer.ex │ ├── printer.ex │ ├── printer_behaviour.ex │ ├── printer_result_types.ex │ ├── tuple_printer.ex │ ├── type_reference_printer.ex │ ├── union_printer.ex │ └── utils │ ├── common_operations.ex │ ├── elm_decoders.ex │ ├── elm_encoders.ex │ ├── elm_fuzzers.ex │ ├── elm_types.ex │ ├── indentation.ex │ └── naming.ex ├── mix.exs ├── mix.lock ├── priv └── templates │ ├── decoders │ ├── enum_decoder.elm.eex │ ├── list_decoder.elm.eex │ ├── product_decoder.elm.eex │ └── sum_decoder.elm.eex │ ├── encoders │ ├── enum_encoder.elm.eex │ ├── list_encoder.elm.eex │ ├── product_encoder.elm.eex │ ├── sum_encoder.elm.eex │ └── tuple_encoder.elm.eex │ ├── fuzzers │ ├── list_fuzzer.elm.eex │ ├── product_fuzzer.elm.eex │ ├── sum_fuzzer.elm.eex │ └── tuple_fuzzer.elm.eex │ ├── preamble │ ├── preamble.elm.eex │ └── tests_preamble.elm.eex │ ├── project │ ├── elm.json.eex │ ├── package.json.eex │ └── tool_versions.eex │ ├── types │ ├── product_type.elm.eex │ └── sum_type.elm.eex │ └── utils │ └── encode.elm.eex └── test ├── printer ├── all_of_printer_test.exs ├── any_of_printer_test.exs ├── array_printer_test.exs ├── enum_printer_test.exs ├── external_references_test.exs ├── object_printer_test.exs ├── one_of_printer_test.exs ├── tuple_printer_test.exs ├── union_printer_test.exs └── utils │ ├── common_operations_test.exs │ ├── elm_decoders_test.exs │ ├── elm_encoders_test.exs │ ├── elm_fuzzers_test.exs │ ├── elm_types_test.exs │ ├── indentation_test.exs │ └── naming_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: mix 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | name: Build and test 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Elixir 18 | uses: erlef/setup-beam@v1 19 | with: 20 | version-type: strict 21 | version-file: .tool-versions 22 | - name: Restore dependencies cache 23 | uses: actions/cache@v2 24 | with: 25 | path: deps 26 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 27 | restore-keys: ${{ runner.os }}-mix- 28 | - name: Install dependencies 29 | run: mix deps.get 30 | - name: Run tests 31 | run: mix test 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The packaged executable 2 | js2e 3 | /js2e_output 4 | node_modules 5 | elm-stuff 6 | package-lock.json 7 | 8 | # The directory Mix will write compiled artifacts to. 9 | /_build 10 | 11 | # If you run "mix test --cover", coverage assets end up here. 12 | /cover 13 | 14 | # The directory Mix downloads your dependencies sources to. 15 | /deps 16 | 17 | # Where 3rd-party dependencies like ExDoc output generated docs. 18 | /doc 19 | 20 | # If the VM crashes, it generates a dump, let's ignore it too. 21 | erl_crash.dump 22 | 23 | # Also ignore archive artifacts (built via "mix archive.build"). 24 | *.ez 25 | 26 | # OS/X 27 | .DS_Store 28 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | erlang 25.1.2 2 | elixir 1.14.3 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | If you have found a bug, think I have misinterpreted the JSON schema spec 4 | somewhere, or have a proposal for a new feature, feel free to open an issue so 5 | we can discuss a proper solution. 6 | 7 | ## Reporting bugs 8 | 9 | When reporting a bug, please include: 10 | 11 | - A short description of the bug, 12 | - JSON schema example that triggers the bug, 13 | - expected Elm output, and the 14 | - actual Elm output. 15 | 16 | ## Pull requests 17 | 18 | Please do not create pull requests before an issue has been created and a 19 | solution has been discussed and agreed upon. 20 | 21 | When making a pull request ensure that: 22 | 23 | 1. It solves one specific problem, and that problem has already been documented 24 | and discussed as an issue, 25 | 2. the PR solves the problem as agreed upon in the issue, 26 | 3. if it is a new feature, ensure that there is proper code coverage of the new 27 | feature, and 28 | 4. the PR contains no compiler warnings or dialyzer warnings (and preferably no 29 | credo warnings). 30 | 31 | ## Development 32 | 33 | The project is written in [Elixir](http://elixir-lang.org/) - as I found it to 34 | be a more suitable tool for the job than Elm - and uses the `mix` tool for 35 | building. 36 | 37 | #### Compiling 38 | 39 | Install dependencies 40 | 41 | mix deps.get 42 | 43 | Compile project 44 | 45 | mix compile 46 | 47 | and you are good to go. 48 | 49 | #### Tests 50 | 51 | Run the standard mix task 52 | 53 | mix test 54 | 55 | for test coverage run 56 | 57 | mix coveralls.html 58 | 59 | #### Static analysis 60 | 61 | Run dialyzer 62 | 63 | mix dialyzer 64 | 65 | Run credo 66 | 67 | mix credo 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Peter Urbak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON Schema to Elm 2 | 3 | ### Description 4 | 5 | Generates Elm types, JSON decoders, JSON encoders, and Fuzz tests from JSON 6 | schema specifications. 7 | 8 | **Only supports - a subset of - JSON Schema draft v7.** 9 | 10 | ## Installation 11 | 12 | This project requires that you already have [elixir](http://elixir-lang.org/) 13 | and its build tool `mix` installed, this can be done with `brew install elixir` 14 | or similar. 15 | 16 | - Download latest release at: 17 | https://github.com/dragonwasrobot/json-schema-to-elm/releases, or 18 | - clone this repository: 19 | `git clone git@github.com:dragonwasrobot/json-schema-to-elm.git`, then 20 | - build an executable: `MIX_ENV=prod mix build` (Windows `cmd.exe`: `set "MIX_ENV=prod" && mix build`), and 21 | - run the executable, `./js2e` (Windows: `escript .\js2e`), that has now been created in your current working 22 | directory. 23 | 24 | ## Usage 25 | 26 | Run `./js2e` for usage instructions. 27 | 28 | > Note: The `js2e` tool only tries to resolve references for the file(s) you 29 | > pass it. So if you need to generate Elm code from more than one file you 30 | > have to pass it the enclosing directory of the relevant JSON schema files, 31 | > in order for it to be able to resolve the references correctly. 32 | 33 | A proper description of which properties are mandatory and how the generator 34 | works is still in progress, but feel free to take a look at the `examples` 35 | folder which contains an example of a pair of JSON schemas and their 36 | corresponding Elm output. Likewise, representations of each of the different 37 | JSON schema types are described in the `lib/types` folder. 38 | 39 | ## Example 40 | 41 | If we supply `js2e` with the following JSON schema file, `definitions.json`: 42 | ``` json 43 | { 44 | "$schema": "http://json-schema.org/draft-07/schema#", 45 | "title": "Definitions", 46 | "$id": "http://example.com/definitions.json", 47 | "description": "Schema for common types", 48 | "definitions": { 49 | "color": { 50 | "$id": "#color", 51 | "type": "string", 52 | "enum": [ "red", "yellow", "green", "blue" ] 53 | }, 54 | "point": { 55 | "$id": "#point", 56 | "type": "object", 57 | "properties": { 58 | "x": { 59 | "type": "number" 60 | }, 61 | "y": { 62 | "type": "number" 63 | } 64 | }, 65 | "required": [ "x", "y" ] 66 | } 67 | } 68 | } 69 | ``` 70 | 71 | it produces the following Elm file, `Data/Definitions.elm`: 72 | 73 | ``` elm 74 | module Data.Definitions exposing 75 | ( Color(..) 76 | , Point 77 | , colorDecoder 78 | , encodeColor 79 | , encodePoint 80 | , pointDecoder 81 | ) 82 | 83 | -- Schema for common types 84 | 85 | import Helper.Encode as Encode 86 | import Json.Decode as Decode exposing (Decoder) 87 | import Json.Decode.Extra as Decode 88 | import Json.Decode.Pipeline 89 | exposing 90 | ( custom 91 | , optional 92 | , required 93 | ) 94 | import Json.Encode as Encode exposing (Value) 95 | 96 | 97 | type Color 98 | = Red 99 | | Yellow 100 | | Green 101 | | Blue 102 | 103 | 104 | type alias Point = 105 | { x : Float 106 | , y : Float 107 | } 108 | 109 | 110 | colorDecoder : Decoder Color 111 | colorDecoder = 112 | Decode.string |> Decode.andThen (parseColor >> Decode.fromResult) 113 | 114 | 115 | parseColor : String -> Result String Color 116 | parseColor color = 117 | case color of 118 | "red" -> 119 | Ok Red 120 | 121 | "yellow" -> 122 | Ok Yellow 123 | 124 | "green" -> 125 | Ok Green 126 | 127 | "blue" -> 128 | Ok Blue 129 | 130 | _ -> 131 | Err <| "Unknown color type: " ++ color 132 | 133 | 134 | pointDecoder : Decoder Point 135 | pointDecoder = 136 | Decode.succeed Point 137 | |> required "x" Decode.float 138 | |> required "y" Decode.float 139 | 140 | 141 | encodeColor : Color -> Value 142 | encodeColor color = 143 | color |> colorToString |> Encode.string 144 | 145 | 146 | colorToString : Color -> String 147 | colorToString color = 148 | case color of 149 | Red -> 150 | "red" 151 | 152 | Yellow -> 153 | "yellow" 154 | 155 | Green -> 156 | "green" 157 | 158 | Blue -> 159 | "blue" 160 | 161 | 162 | encodePoint : Point -> Value 163 | encodePoint point = 164 | [] 165 | |> Encode.required "x" point.x Encode.float 166 | |> Encode.required "y" point.y Encode.float 167 | |> Encode.object 168 | ``` 169 | 170 | which contains an Elm type for the `color` and `point` definitions along with 171 | their corresponding JSON decoders and encoders. 172 | 173 | Furthermore, if we instead supply `js2e` with a directory of JSON schema files 174 | that have references across files, e.g. 175 | 176 | ``` json 177 | { 178 | "$schema": "http://json-schema.org/draft-07/schema#", 179 | "$id": "http://example.com/circle.json", 180 | "title": "Circle", 181 | "description": "Schema for a circle shape", 182 | "type": "object", 183 | "properties": { 184 | "center": { 185 | "$ref": "http://example.com/definitions.json#point" 186 | }, 187 | "radius": { 188 | "type": "number" 189 | }, 190 | "color": { 191 | "$ref": "http://example.com/definitions.json#color" 192 | } 193 | }, 194 | "required": ["center", "radius"] 195 | } 196 | ``` 197 | 198 | then the corresponding Elm file, `Data/Circle.elm`, will import the 199 | definitions (types, encoders and decoders) from the other Elm module, 200 | `Data/Definitions.elm`. 201 | 202 | ``` elm 203 | module Data.Circle exposing 204 | ( Circle 205 | , circleDecoder 206 | , encodeCircle 207 | ) 208 | 209 | -- Schema for a circle shape 210 | 211 | import Data.Definitions as Definitions 212 | import Helper.Encode as Encode 213 | import Json.Decode as Decode exposing (Decoder) 214 | import Json.Decode.Extra as Decode 215 | import Json.Decode.Pipeline 216 | exposing 217 | ( custom 218 | , optional 219 | , required 220 | ) 221 | import Json.Encode as Encode exposing (Value) 222 | 223 | 224 | type alias Circle = 225 | { center : Definitions.Point 226 | , color : Maybe Definitions.Color 227 | , radius : Float 228 | } 229 | 230 | 231 | circleDecoder : Decoder Circle 232 | circleDecoder = 233 | succeed Circle 234 | |> required "center" Definitions.pointDecoder 235 | |> optional "color" (Decode.nullable Definitions.colorDecoder) Nothing 236 | |> required "radius" Decode.float 237 | 238 | 239 | encodeCircle : Circle -> Value 240 | encodeCircle circle = 241 | [] 242 | |> Encode.required "center" circle.center Definitions.encodePoint 243 | |> Encode.optional "color" circle.color Definitions.encodeColor 244 | |> Encode.required "radius" circle.radius Encode.float 245 | |> Encode.object 246 | ``` 247 | 248 | Furthermore, `js2e` also generates test files for the generated decoders and 249 | encoders to make the generated code immediately testable. The generated test 250 | files fuzzes instances of a given Elm type and tests that encoding it as JSON 251 | and decoding it back into Elm returns the original instance of that generated 252 | Elm type. In the above case, the following test files, 253 | `tests/Data/CircleTests.elm` and `tests/Data/DefinitionsTests.elm`, are 254 | generated: 255 | 256 | ``` elm 257 | module Data.CircleTests exposing 258 | ( circleFuzzer 259 | , encodeDecodeCircleTest 260 | ) 261 | 262 | 263 | -- Tests: Schema for a circle shape 264 | 265 | import Data.Circle exposing (..) 266 | import Data.DefinitionsTests as Definitions 267 | import Expect exposing (Expectation) 268 | import Fuzz exposing (Fuzzer) 269 | import Json.Decode as Decode 270 | import Test exposing (..) 271 | 272 | 273 | circleFuzzer : Fuzzer Circle 274 | circleFuzzer = 275 | Fuzz.map3 276 | Circle 277 | Definitions.pointFuzzer 278 | (Fuzz.maybe Definitions.colorFuzzer) 279 | Fuzz.niceFloat 280 | 281 | 282 | encodeDecodeCircleTest : Test 283 | encodeDecodeCircleTest = 284 | fuzz circleFuzzer "can encode and decode Circle object" <| 285 | \circle -> 286 | circle 287 | |> encodeCircle 288 | |> Decode.decodeValue circleDecoder 289 | |> Expect.equal (Ok circle) 290 | ``` 291 | and 292 | 293 | ``` elm 294 | module Data.DefinitionsTests exposing 295 | ( colorFuzzer 296 | , encodeDecodeColorTest 297 | , encodeDecodePointTest 298 | , pointFuzzer 299 | ) 300 | 301 | -- Tests: Schema for common types 302 | 303 | import Data.Definitions exposing (..) 304 | import Expect exposing (Expectation) 305 | import Fuzz exposing (Fuzzer) 306 | import Json.Decode as Decode 307 | import Test exposing (..) 308 | 309 | 310 | colorFuzzer : Fuzzer Color 311 | colorFuzzer = 312 | [ Red, Yellow, Green, Blue ] 313 | |> List.map Fuzz.constant 314 | |> Fuzz.oneOf 315 | 316 | 317 | encodeDecodeColorTest : Test 318 | encodeDecodeColorTest = 319 | fuzz colorFuzzer "can encode and decode Color object" <| 320 | \color -> 321 | color 322 | |> encodeColor 323 | |> Decode.decodeValue colorDecoder 324 | |> Expect.equal (Ok color) 325 | 326 | 327 | pointFuzzer : Fuzzer Point 328 | pointFuzzer = 329 | Fuzz.map2 330 | Point 331 | Fuzz.niceFloat 332 | Fuzz.niceFloat 333 | 334 | 335 | encodeDecodePointTest : Test 336 | encodeDecodePointTest = 337 | fuzz pointFuzzer "can encode and decode Point object" <| 338 | \point -> 339 | point 340 | |> encodePoint 341 | |> Decode.decodeValue pointDecoder 342 | |> Expect.equal (Ok point) 343 | ``` 344 | 345 | Finally, `js2e` also generates package config files, `package.json` and 346 | `elm.json`, and a `.tool-versions` file, making it easy to test that the 347 | generated Elm code is behaving as expected. Note that the `.tool-versions` file 348 | is not a file required by `elm` nor `elm-test` but instead a file used by the 349 | `asdf` version manager, https://github.com/asdf-vm/asdf, to install and run the 350 | correct compiler versions of `node` and `elm` specified in the `.tool-versions` 351 | file for a given project. 352 | 353 | Thus, if we supply the following directory structure to `js2e` in the above 354 | case: 355 | 356 | ``` 357 | . 358 | └── js2e_input/ 359 | ├── definitions.json 360 | └── circle.json 361 | ``` 362 | 363 | the following new directory structure is generated: 364 | 365 | ``` 366 | . 367 | └── js2e_output/ 368 | ├── .tool-versions 369 | ├── package.json 370 | ├── elm.json 371 | ├── src/ 372 | │ ├── Data/ 373 | │ │ ├── Circle.elm 374 | │ │ └── Definitions.elm 375 | │ └── Helper/ 376 | │ └── Encode.elm 377 | └── tests/ 378 | └── Data/ 379 | ├── CircleTests.elm 380 | └── DefinitionsTests.elm 381 | ``` 382 | 383 | containing the files described above along with the needed package config files 384 | to compile and run the tests. 385 | 386 | ## Error reporting 387 | 388 | Any errors encountered by the `js2e` tool while parsing the JSON schema files or 389 | printing the Elm code output, is reported in an Elm-like style, e.g. 390 | 391 | ``` 392 | --- UNKNOWN NODE TYPE -------------------------------------- all_of_example.json 393 | 394 | The value of "type" at '#/allOf/0/properties/description' did not match a known node type 395 | 396 | "type": "strink" 397 | ^^^^^^^^ 398 | 399 | Was expecting one of the following types 400 | 401 | ["null", "boolean", "object", "array", "number", "integer", "string"] 402 | 403 | Hint: See the specification section 6.25. "Validation keywords - type" 404 | 405 | ``` 406 | 407 | or 408 | 409 | ``` 410 | --- UNRESOLVED REFERENCE ----------------------------------- all_of_example.json 411 | 412 | 413 | The following reference at `#/allOf/0/color` could not be resolved 414 | 415 | "$ref": #/definitions/kolor 416 | ^^^^^^^^^^^^^^^^^^^ 417 | 418 | 419 | Hint: See the specification section 9. "Base URI and dereferencing" 420 | 421 | ``` 422 | 423 | If you encounter an error while using `js2e` that does not mimic the above 424 | Elm-like style, but instead looks like an Elixir stacktrace, please report this 425 | as a bug by opening an issue and including a JSON schema example that recreates 426 | the error. 427 | 428 | ## Contributing 429 | 430 | If you feel like something is missing/wrong or if I've misinterpreted the JSON 431 | schema spec, feel free to open an issue so we can discuss a solution. Note that 432 | the JSON schema parser has been moved to the new project, 433 | https://github.com/dragonwasrobot/json_schema, so this repo only implements the 434 | Elm code generators. 435 | 436 | Please consult `CONTRIBUTING.md` first before opening an issue. 437 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :elixir, ansi_enabled: true 4 | 5 | config :js2e, templates_location: "./priv/templates/" 6 | config :js2e, output_location: "js2e_output" 7 | 8 | import_config "#{Mix.env()}.exs" 9 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :logger, level: :debug 4 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :logger, level: :warn 4 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :logger, level: :debug 4 | -------------------------------------------------------------------------------- /coveralls.json: -------------------------------------------------------------------------------- 1 | { 2 | "coverage_options": { 3 | "treat_no_relevant_lines_as_covered": true, 4 | "minimum_coverage": 70 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/example-input-json-schemas/circle.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "http://example.com/circle.json", 4 | "title": "Circle", 5 | "description": "Schema for a circle shape", 6 | "type": "object", 7 | "properties": { 8 | "radius": { 9 | "type": "number" 10 | }, 11 | "center": { 12 | "$ref": "http://example.com/definitions.json#point" 13 | }, 14 | "color": { 15 | "$ref": "http://example.com/definitions.json#color" 16 | } 17 | }, 18 | "required": [ 19 | "center", 20 | "radius" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /examples/example-input-json-schemas/definitions.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "http://example.com/definitions.json", 4 | "title": "Definitions", 5 | "description": "Schema for common types", 6 | "definitions": { 7 | "color": { 8 | "$id": "#color", 9 | "type": "string", 10 | "enum": [ 11 | "red", 12 | "yellow", 13 | "green", 14 | "blue" 15 | ], 16 | "default": "green" 17 | }, 18 | "point": { 19 | "$id": "#point", 20 | "type": "object", 21 | "properties": { 22 | "x": { 23 | "type": "number" 24 | }, 25 | "y": { 26 | "type": "number" 27 | } 28 | }, 29 | "required": [ 30 | "x", 31 | "y" 32 | ] 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/example-output-elm-code/.tool-versions: -------------------------------------------------------------------------------- 1 | elm 0.19.1 2 | node 16.17.0 3 | -------------------------------------------------------------------------------- /examples/example-output-elm-code/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "NoRedInk/elm-json-decode-pipeline": "1.0.1", 10 | "elm/browser": "1.0.2", 11 | "elm/core": "1.0.5", 12 | "elm/html": "1.0.0", 13 | "elm-community/json-extra": "4.3.0" 14 | }, 15 | "indirect": { 16 | "elm/json": "1.1.3", 17 | "elm/parser": "1.1.0", 18 | "elm/time": "1.0.0", 19 | "elm/url": "1.0.0", 20 | "elm/virtual-dom": "1.0.3", 21 | "rtfeldman/elm-iso8601-date-strings": "1.1.4" 22 | } 23 | }, 24 | "test-dependencies": { 25 | "direct": { 26 | "elm-explorations/test": "2.1.1" 27 | }, 28 | "indirect": { 29 | "elm/bytes": "1.0.8", 30 | "elm/random": "1.0.0" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/example-output-elm-code/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js2e_output", 3 | "version": "1.0.0", 4 | "description": "Autogenerated Elm types, decoders, and encoders", 5 | "dependencies": { 6 | "elm-test": "^0.19.1-revision12" 7 | }, 8 | "scripts": { 9 | "test": "./node_modules/elm-test/bin/elm-test --fuzz 10" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/example-output-elm-code/src/Data/Circle.elm: -------------------------------------------------------------------------------- 1 | module Data.Circle exposing (..) 2 | 3 | -- Schema for a circle shape 4 | 5 | import Helper.Encode as Encode 6 | import Json.Decode as Decode exposing (Decoder) 7 | import Json.Decode.Extra as Decode 8 | import Json.Decode.Pipeline 9 | exposing 10 | ( custom 11 | , optional 12 | , required 13 | ) 14 | import Json.Encode as Encode exposing (Value) 15 | import Data.Definitions as Definitions 16 | 17 | 18 | type alias Circle = 19 | { center : Definitions.Point 20 | , color : Maybe Definitions.Color 21 | , radius : Float 22 | } 23 | 24 | 25 | circleDecoder : Decoder Circle 26 | circleDecoder = 27 | Decode.succeed Circle 28 | |> required "center" Definitions.pointDecoder 29 | |> optional "color" (Decode.nullable Definitions.colorDecoder) Nothing 30 | |> required "radius" Decode.float 31 | 32 | 33 | encodeCircle : Circle -> Value 34 | encodeCircle circle = 35 | [] 36 | |> Encode.required "center" circle.center Definitions.encodePoint 37 | |> Encode.optional "color" circle.color Definitions.encodeColor 38 | |> Encode.required "radius" circle.radius Encode.float 39 | |> Encode.object 40 | -------------------------------------------------------------------------------- /examples/example-output-elm-code/src/Data/Definitions.elm: -------------------------------------------------------------------------------- 1 | module Data.Definitions exposing (..) 2 | 3 | -- Schema for common types 4 | 5 | import Helper.Encode as Encode 6 | import Json.Decode as Decode exposing (Decoder) 7 | import Json.Decode.Extra as Decode 8 | import Json.Decode.Pipeline 9 | exposing 10 | ( custom 11 | , optional 12 | , required 13 | ) 14 | import Json.Encode as Encode exposing (Value) 15 | 16 | 17 | type Color 18 | = Red 19 | | Yellow 20 | | Green 21 | | Blue 22 | 23 | 24 | type alias Point = 25 | { x : Float 26 | , y : Float 27 | } 28 | 29 | 30 | colorDecoder : Decoder Color 31 | colorDecoder = 32 | Decode.string |> Decode.andThen (parseColor >> Decode.fromResult) 33 | 34 | 35 | parseColor : String -> Result String Color 36 | parseColor color = 37 | case color of 38 | "red" -> 39 | Ok Red 40 | 41 | "yellow" -> 42 | Ok Yellow 43 | 44 | "green" -> 45 | Ok Green 46 | 47 | "blue" -> 48 | Ok Blue 49 | 50 | _ -> 51 | Err <| "Unknown color type: " ++ color 52 | 53 | 54 | pointDecoder : Decoder Point 55 | pointDecoder = 56 | Decode.succeed Point 57 | |> required "x" Decode.float 58 | |> required "y" Decode.float 59 | 60 | 61 | encodeColor : Color -> Value 62 | encodeColor color = 63 | color |> colorToString |> Encode.string 64 | 65 | 66 | colorToString : Color -> String 67 | colorToString color = 68 | case color of 69 | Red -> 70 | "red" 71 | 72 | Yellow -> 73 | "yellow" 74 | 75 | Green -> 76 | "green" 77 | 78 | Blue -> 79 | "blue" 80 | 81 | 82 | encodePoint : Point -> Value 83 | encodePoint point = 84 | [] 85 | |> Encode.required "x" point.x Encode.float 86 | |> Encode.required "y" point.y Encode.float 87 | |> Encode.object 88 | -------------------------------------------------------------------------------- /examples/example-output-elm-code/src/Helper/Encode.elm: -------------------------------------------------------------------------------- 1 | module Helper.Encode exposing (optional, required) 2 | 3 | -- Helper functions for encoding JSON objects. 4 | 5 | import Json.Encode as Encode exposing (Value) 6 | 7 | 8 | required : 9 | String 10 | -> a 11 | -> (a -> Value) 12 | -> List ( String, Value ) 13 | -> List ( String, Value ) 14 | required key value encode properties = 15 | properties ++ [ ( key, encode value ) ] 16 | 17 | 18 | optional : 19 | String 20 | -> Maybe a 21 | -> (a -> Value) 22 | -> List ( String, Value ) 23 | -> List ( String, Value ) 24 | optional key maybe encode properties = 25 | case maybe of 26 | Just value -> 27 | properties ++ [ ( key, encode value ) ] 28 | 29 | Nothing -> 30 | properties 31 | -------------------------------------------------------------------------------- /examples/example-output-elm-code/tests/Data/CircleTests.elm: -------------------------------------------------------------------------------- 1 | module Data.CircleTests exposing (..) 2 | 3 | -- Tests: Schema for a circle shape 4 | 5 | import Expect exposing (Expectation) 6 | import Fuzz exposing (Fuzzer) 7 | import Test exposing (..) 8 | import Json.Decode as Decode 9 | import Data.Circle exposing (..) 10 | import Data.DefinitionsTests as Definitions 11 | 12 | 13 | circleFuzzer : Fuzzer Circle 14 | circleFuzzer = 15 | Fuzz.map3 16 | Circle 17 | Definitions.pointFuzzer 18 | (Fuzz.maybe Definitions.colorFuzzer) 19 | Fuzz.niceFloat 20 | 21 | 22 | encodeDecodeCircleTest : Test 23 | encodeDecodeCircleTest = 24 | fuzz circleFuzzer "can encode and decode Circle object" <| 25 | \circle -> 26 | circle 27 | |> encodeCircle 28 | |> Decode.decodeValue circleDecoder 29 | |> Expect.equal (Ok circle) 30 | -------------------------------------------------------------------------------- /examples/example-output-elm-code/tests/Data/DefinitionsTests.elm: -------------------------------------------------------------------------------- 1 | module Data.DefinitionsTests exposing (..) 2 | 3 | -- Tests: Schema for common types 4 | 5 | import Expect exposing (Expectation) 6 | import Fuzz exposing (Fuzzer) 7 | import Test exposing (..) 8 | import Json.Decode as Decode 9 | import Data.Definitions exposing (..) 10 | 11 | 12 | colorFuzzer : Fuzzer Color 13 | colorFuzzer = 14 | Fuzz.oneOf 15 | [ Fuzz.constant Red 16 | , Fuzz.constant Yellow 17 | , Fuzz.constant Green 18 | , Fuzz.constant Blue 19 | ] 20 | 21 | 22 | encodeDecodeColorTest : Test 23 | encodeDecodeColorTest = 24 | fuzz colorFuzzer "can encode and decode Color" <| 25 | \color -> 26 | color 27 | |> encodeColor 28 | |> Decode.decodeValue colorDecoder 29 | |> Expect.equal (Ok color) 30 | 31 | 32 | pointFuzzer : Fuzzer Point 33 | pointFuzzer = 34 | Fuzz.map2 35 | Point 36 | Fuzz.niceFloat 37 | Fuzz.niceFloat 38 | 39 | 40 | encodeDecodePointTest : Test 41 | encodeDecodePointTest = 42 | fuzz pointFuzzer "can encode and decode Point object" <| 43 | \point -> 44 | point 45 | |> encodePoint 46 | |> Decode.decodeValue pointDecoder 47 | |> Expect.equal (Ok point) 48 | -------------------------------------------------------------------------------- /lib/js2e.ex: -------------------------------------------------------------------------------- 1 | defmodule JS2E do 2 | @moduledoc ~S""" 3 | Transforms one or more JSON schema files into their corresponding 4 | Elm types and JSON decoders. 5 | 6 | Expects a PATH to one or more JSON schema files from which to generate 7 | Elm code. 8 | 9 | js2e PATH [--module-name NAME] 10 | 11 | The JSON schema(s) at the given PATH will be converted to Elm types 12 | and JSON decoders. 13 | 14 | ## Options 15 | 16 | * `--module-name` - the module name prefix for the printed Elm modules \ 17 | default value is 'Data'. 18 | """ 19 | 20 | require Logger 21 | alias JS2E.Printer 22 | alias JsonSchema.Parser.{ParserError, ParserWarning} 23 | alias Printer.PrinterError 24 | 25 | @output_location Application.compile_env(:js2e, :output_location) 26 | 27 | @spec main([String.t()]) :: :ok 28 | def main(args) do 29 | {options, paths, errors} = OptionParser.parse(args, switches: [module_name: :string]) 30 | 31 | if Enum.empty?(paths) == true do 32 | IO.puts(@moduledoc) 33 | exit(:normal) 34 | end 35 | 36 | if length(errors) > 0 do 37 | print_error("Error: Found one or more errors in the supplied options") 38 | exit({:unknown_arguments, errors}) 39 | end 40 | 41 | files = resolve_all_paths(paths) 42 | 43 | if Enum.empty?(files) == true do 44 | print_error("Error: Could not find any JSON files in path: #{inspect(paths)}") 45 | 46 | exit(:no_files) 47 | end 48 | 49 | output_path = create_output_dir(options) 50 | generate(files, output_path) 51 | end 52 | 53 | @spec resolve_all_paths([String.t()]) :: [Path.t()] 54 | defp resolve_all_paths(paths) do 55 | paths 56 | |> Enum.filter(&File.exists?/1) 57 | |> Enum.reduce([], fn filename, files -> 58 | cond do 59 | File.dir?(filename) -> 60 | walk_directory(filename) ++ files 61 | 62 | String.ends_with?(filename, ".json") -> 63 | [filename | files] 64 | 65 | true -> 66 | files 67 | end 68 | end) 69 | end 70 | 71 | @spec walk_directory(String.t()) :: [String.t()] 72 | defp walk_directory(dir) do 73 | dir 74 | |> File.ls!() 75 | |> Enum.reduce([], fn file, files -> 76 | filename = "#{dir}/#{file}" 77 | 78 | cond do 79 | File.dir?(filename) -> 80 | walk_directory(filename) ++ files 81 | 82 | String.ends_with?(file, ".json") -> 83 | [filename | files] 84 | 85 | true -> 86 | files 87 | end 88 | end) 89 | end 90 | 91 | @spec create_output_dir(list) :: String.t() 92 | defp create_output_dir(options) do 93 | module_name = 94 | if Keyword.has_key?(options, :module_name) do 95 | Keyword.get(options, :module_name) 96 | else 97 | "Data" 98 | end 99 | 100 | "#{@output_location}/src" 101 | |> Path.join(module_name) 102 | |> String.replace(".", "/") 103 | |> File.mkdir_p!() 104 | 105 | "#{@output_location}/src/Helper" 106 | |> File.mkdir_p!() 107 | 108 | "#{@output_location}/tests" 109 | |> Path.join(module_name) 110 | |> String.replace(".", "/") 111 | |> File.mkdir_p!() 112 | 113 | module_name 114 | end 115 | 116 | @spec generate([String.t()], String.t()) :: :ok 117 | def generate(schema_paths, module_name) do 118 | Logger.info("Parsing JSON schema files!") 119 | parser_result = JsonSchema.parse_schema_files(schema_paths) 120 | 121 | pretty_parser_warnings(parser_result.warnings) 122 | 123 | if length(parser_result.errors) > 0 do 124 | pretty_parser_errors(parser_result.errors) 125 | else 126 | Logger.info("Converting to Elm code!") 127 | 128 | printer_result = Printer.print_schemas(parser_result.schema_dict, module_name) 129 | 130 | tests_printer_result = Printer.print_schemas_tests(parser_result.schema_dict, module_name) 131 | 132 | cond do 133 | length(printer_result.errors) > 0 -> 134 | pretty_printer_errors(printer_result.errors) 135 | 136 | length(tests_printer_result.errors) > 0 -> 137 | pretty_printer_errors(tests_printer_result.errors) 138 | 139 | true -> 140 | Logger.info("Printing Elm code to file(s)!") 141 | 142 | file_dict = printer_result.file_dict 143 | 144 | Enum.each(file_dict, fn {file_path, file_content} -> 145 | normalized_file_path = 146 | String.replace( 147 | file_path, 148 | module_name, 149 | String.replace(module_name, ".", "/") 150 | ) 151 | 152 | Logger.debug(fn -> "Writing file '#{normalized_file_path}'" end) 153 | {:ok, file} = File.open(normalized_file_path, [:write]) 154 | IO.binwrite(file, file_content) 155 | File.close(file) 156 | Logger.info("Created file '#{normalized_file_path}'") 157 | end) 158 | 159 | tests_file_dict = tests_printer_result.file_dict 160 | 161 | Enum.each(tests_file_dict, fn {file_path, file_content} -> 162 | normalized_file_path = 163 | String.replace( 164 | file_path, 165 | module_name, 166 | String.replace(module_name, ".", "/") 167 | ) 168 | 169 | Logger.debug("Writing file '#{normalized_file_path}'") 170 | {:ok, file} = File.open(normalized_file_path, [:write]) 171 | IO.binwrite(file, file_content) 172 | File.close(file) 173 | Logger.info("Created file '#{normalized_file_path}'") 174 | end) 175 | 176 | IO.puts(""" 177 | Elm types, decoders, encoders and tests 178 | written to '#{@output_location}'. 179 | 180 | Go to '#{@output_location}' and run 181 | 182 | $ npm install 183 | 184 | followed by 185 | 186 | $ npm test 187 | 188 | in order to run 'elm-test' test suite 189 | on the generated Elm source code. 190 | """) 191 | end 192 | end 193 | end 194 | 195 | @spec pretty_parser_warnings([{Path.t(), [ParserWarning.t()]}]) :: :ok 196 | defp pretty_parser_warnings(warnings) do 197 | warnings 198 | |> Enum.each(fn {file_path, warnings} -> 199 | if length(warnings) > 0 do 200 | warning_header() 201 | 202 | warnings 203 | |> Enum.group_by(fn warning -> warning.warning_type end) 204 | |> Enum.each(fn {warning_type, warnings} -> 205 | pretty_warning_type = 206 | warning_type 207 | |> to_string 208 | |> String.replace("_", " ") 209 | |> String.downcase() 210 | 211 | padding = 212 | String.duplicate( 213 | "-", 214 | max( 215 | 0, 216 | 74 - String.length(pretty_warning_type) - 217 | String.length(file_path) 218 | ) 219 | ) 220 | 221 | warnings 222 | |> Enum.each(fn warning -> 223 | print_header("--- #{pretty_warning_type} #{padding} #{file_path}\n") 224 | IO.puts(warning.message) 225 | end) 226 | end) 227 | end 228 | end) 229 | 230 | :ok 231 | end 232 | 233 | @spec pretty_parser_errors([{Path.t(), [ParserError.t()]}]) :: :ok 234 | defp pretty_parser_errors(errors) do 235 | errors 236 | |> Enum.each(fn {file_path, errors} -> 237 | if length(errors) > 0 do 238 | errors 239 | |> Enum.group_by(fn err -> err.error_type end) 240 | |> Enum.each(fn {error_type, errors} -> 241 | pretty_error_type = 242 | error_type 243 | |> to_string 244 | |> String.replace("_", " ") 245 | |> String.upcase() 246 | 247 | padding = 248 | String.duplicate( 249 | "-", 250 | max( 251 | 0, 252 | 74 - String.length(pretty_error_type) - String.length(file_path) 253 | ) 254 | ) 255 | 256 | errors 257 | |> Enum.each(fn error -> 258 | print_header("--- #{pretty_error_type} #{padding} #{file_path}\n") 259 | IO.puts(error.message) 260 | end) 261 | end) 262 | end 263 | end) 264 | 265 | :ok 266 | end 267 | 268 | @spec pretty_printer_errors([PrinterError.t()]) :: :ok 269 | defp pretty_printer_errors(errors) do 270 | errors 271 | |> Enum.each(fn {file_path, errors} -> 272 | if length(errors) > 0 do 273 | errors 274 | |> Enum.group_by(fn err -> err.error_type end) 275 | |> Enum.each(fn {error_type, errors} -> 276 | pretty_error_type = 277 | error_type 278 | |> to_string 279 | |> String.replace("_", " ") 280 | |> String.upcase() 281 | 282 | padding = 283 | String.duplicate( 284 | "-", 285 | max( 286 | 0, 287 | 74 - String.length(pretty_error_type) - String.length(file_path) 288 | ) 289 | ) 290 | 291 | errors 292 | |> Enum.each(fn error -> 293 | print_header("--- #{pretty_error_type} #{padding} #{file_path}\n") 294 | IO.puts(error.message) 295 | end) 296 | end) 297 | end 298 | end) 299 | 300 | :ok 301 | end 302 | 303 | defp print_error(str) do 304 | IO.puts(IO.ANSI.format([:cyan, str])) 305 | end 306 | 307 | defp print_header(str) do 308 | IO.puts(IO.ANSI.format([:cyan, str])) 309 | end 310 | 311 | defp warning_header do 312 | header = String.duplicate("^", 35) <> " WARNINGS " <> String.duplicate("^", 35) 313 | 314 | IO.puts(IO.ANSI.format([:yellow, header])) 315 | end 316 | end 317 | -------------------------------------------------------------------------------- /lib/printer/any_of_printer.ex: -------------------------------------------------------------------------------- 1 | defmodule JS2E.Printer.AnyOfPrinter do 2 | @behaviour JS2E.Printer.PrinterBehaviour 3 | @moduledoc """ 4 | A printer for printing an 'any of' type decoder. 5 | """ 6 | 7 | require Elixir.{EEx, Logger} 8 | alias JS2E.Printer 9 | alias JsonSchema.{Parser, Resolver, Types} 10 | alias Parser.ParserError 11 | alias Printer.{PrinterError, PrinterResult, Utils} 12 | alias Types.{AnyOfType, SchemaDefinition} 13 | 14 | alias Utils.{ 15 | CommonOperations, 16 | ElmDecoders, 17 | ElmEncoders, 18 | ElmFuzzers, 19 | ElmTypes, 20 | Indentation, 21 | Naming 22 | } 23 | 24 | @templates_location Application.compile_env(:js2e, :templates_location) 25 | 26 | # Type 27 | 28 | @type_location Path.join(@templates_location, "types/product_type.elm.eex") 29 | EEx.function_from_file(:defp, :type_template, @type_location, [ 30 | :product_type 31 | ]) 32 | 33 | @impl JS2E.Printer.PrinterBehaviour 34 | @spec print_type( 35 | Types.typeDefinition(), 36 | SchemaDefinition.t(), 37 | Types.schemaDictionary(), 38 | String.t() 39 | ) :: PrinterResult.t() 40 | def print_type( 41 | %AnyOfType{name: name, path: path, types: types}, 42 | schema_def, 43 | schema_dict, 44 | module_name 45 | ) do 46 | normalized_name = Naming.normalize_identifier(name, :downcase) 47 | type_name = Naming.upcase_first(normalized_name) 48 | 49 | {type_fields, errors} = 50 | types 51 | |> Enum.reduce({[], []}, fn type, {acc_type_fields, acc_errors} -> 52 | case create_type_fields( 53 | type, 54 | path, 55 | schema_def, 56 | schema_dict, 57 | module_name 58 | ) do 59 | {:ok, type_fields} -> 60 | {type_fields ++ acc_type_fields, acc_errors} 61 | 62 | {:error, error} -> 63 | {acc_type_fields, [error | acc_errors]} 64 | end 65 | end) 66 | 67 | type_fields = type_fields |> List.flatten() |> Enum.sort_by(& &1.name) 68 | 69 | %{name: type_name, fields: {:named, List.flatten(type_fields)}} 70 | |> type_template() 71 | |> PrinterResult.new(errors) 72 | end 73 | 74 | @spec create_type_fields( 75 | URI.t(), 76 | URI.t(), 77 | SchemaDefinition.t(), 78 | Types.schemaDictionary(), 79 | String.t() 80 | ) :: {:ok, [ElmTypes.named_field()]} | {:error, PrinterError.t() | ParserError.t()} 81 | defp create_type_fields( 82 | type_path, 83 | parent, 84 | schema_def, 85 | schema_dict, 86 | module_name 87 | ) do 88 | with {:ok, {resolved_type, resolved_schema}} <- 89 | Resolver.resolve_type(type_path, parent, schema_def, schema_dict), 90 | {:ok, type_fields} <- 91 | ElmTypes.create_fields( 92 | :anonymous, 93 | resolved_type, 94 | resolved_schema, 95 | parent, 96 | schema_def, 97 | schema_dict, 98 | module_name 99 | ) do 100 | type_fields = type_fields |> Enum.map(&make_optional(&1)) 101 | 102 | {:ok, type_fields} 103 | else 104 | {:error, error} -> 105 | {:error, error} 106 | end 107 | end 108 | 109 | @spec make_optional(ElmTypes.named_field()) :: ElmTypes.named_field() 110 | defp make_optional(field) do 111 | %{field | type: "Maybe #{field.type}"} 112 | end 113 | 114 | # Decoder 115 | 116 | @decoder_location Path.join(@templates_location, "decoders/product_decoder.elm.eex") 117 | EEx.function_from_file(:defp, :decoder_template, @decoder_location, [ 118 | :product_decoder 119 | ]) 120 | 121 | @impl JS2E.Printer.PrinterBehaviour 122 | @spec print_decoder( 123 | Types.typeDefinition(), 124 | SchemaDefinition.t(), 125 | Types.schemaDictionary(), 126 | String.t() 127 | ) :: PrinterResult.t() 128 | def print_decoder( 129 | %AnyOfType{name: name, path: path, types: type_paths}, 130 | schema_def, 131 | schema_dict, 132 | module_name 133 | ) do 134 | {decoder_clauses, errors} = 135 | type_paths 136 | |> Enum.map(&create_decoder_property(&1, path, schema_def, schema_dict, module_name)) 137 | |> CommonOperations.split_ok_and_errors() 138 | 139 | normalized_name = Naming.normalize_identifier(name, :downcase) 140 | decoder_name = "#{normalized_name}Decoder" 141 | type_name = Naming.upcase_first(normalized_name) 142 | 143 | decoder_clauses = 144 | decoder_clauses 145 | |> List.flatten() 146 | |> Enum.sort_by(& &1.property_name) 147 | 148 | %{name: decoder_name, type: type_name, clauses: {:named, decoder_clauses}} 149 | |> decoder_template() 150 | |> PrinterResult.new(errors) 151 | end 152 | 153 | @spec create_decoder_property( 154 | URI.t(), 155 | URI.t(), 156 | SchemaDefinition.t(), 157 | Types.schemaDictionary(), 158 | String.t() 159 | ) :: 160 | {:ok, [ElmDecoders.named_product_clause()]} 161 | | {:error, PrinterError.t() | ParserError.t()} 162 | defp create_decoder_property( 163 | type_path, 164 | parent, 165 | schema_def, 166 | schema_dict, 167 | module_name 168 | ) do 169 | with {:ok, {resolved_type, resolved_schema_def}} <- 170 | Resolver.resolve_type(type_path, parent, schema_def, schema_dict), 171 | {:ok, decoder_clauses} <- 172 | ElmDecoders.create_clauses( 173 | :anonymous, 174 | resolved_type, 175 | resolved_schema_def, 176 | [], 177 | schema_def, 178 | schema_dict, 179 | module_name 180 | ) do 181 | decoder_clauses = 182 | decoder_clauses 183 | |> Enum.map(fn clause -> 184 | if clause.property_name == resolved_type.name do 185 | %{ 186 | clause 187 | | option: :custom, 188 | decoder_name: "(Decode.nullable #{clause.decoder_name})" 189 | } 190 | else 191 | %{ 192 | clause 193 | | option: :optional, 194 | decoder_name: "(Decode.nullable #{clause.decoder_name}) Nothing" 195 | } 196 | end 197 | end) 198 | 199 | {:ok, decoder_clauses} 200 | else 201 | {:error, error} -> 202 | {:error, error} 203 | end 204 | end 205 | 206 | # Encoder 207 | 208 | @encoder_location Path.join(@templates_location, "encoders/product_encoder.elm.eex") 209 | EEx.function_from_file(:defp, :encoder_template, @encoder_location, [ 210 | :product_encoder 211 | ]) 212 | 213 | @impl JS2E.Printer.PrinterBehaviour 214 | @spec print_encoder( 215 | Types.typeDefinition(), 216 | SchemaDefinition.t(), 217 | Types.schemaDictionary(), 218 | String.t() 219 | ) :: PrinterResult.t() 220 | def print_encoder( 221 | %AnyOfType{name: name, path: path, types: type_paths}, 222 | schema_def, 223 | schema_dict, 224 | module_name 225 | ) do 226 | {encoder_properties, errors} = 227 | type_paths 228 | |> Enum.map(&create_encoder_property(&1, path, schema_def, schema_dict, module_name)) 229 | |> List.flatten() 230 | |> CommonOperations.split_ok_and_errors() 231 | 232 | argument_name = Naming.normalize_identifier(name, :downcase) 233 | type_name = Naming.upcase_first(argument_name) 234 | encoder_name = "encode#{type_name}" 235 | 236 | %{ 237 | name: encoder_name, 238 | type: type_name, 239 | argument_name: argument_name, 240 | properties: encoder_properties 241 | } 242 | |> encoder_template() 243 | |> Indentation.trim_newlines() 244 | |> PrinterResult.new(errors) 245 | end 246 | 247 | @spec create_encoder_property( 248 | Types.typeIdentifier(), 249 | URI.t(), 250 | SchemaDefinition.t(), 251 | Types.schemaDictionary(), 252 | String.t() 253 | ) :: {:ok, ElmEncoders.product_encoder()} | {:error, PrinterError.t() | ParserError.t()} 254 | defp create_encoder_property( 255 | type_path, 256 | parent, 257 | schema_def, 258 | schema_dict, 259 | module_name 260 | ) do 261 | case Resolver.resolve_type(type_path, parent, schema_def, schema_dict) do 262 | {:ok, {resolved_type, resolved_schema}} -> 263 | to_encoder_property(resolved_type, resolved_schema, schema_dict, module_name) 264 | 265 | {:error, error} -> 266 | {:error, error} 267 | end 268 | end 269 | 270 | @spec to_encoder_property( 271 | Types.typeDefinition(), 272 | SchemaDefinition.t(), 273 | Types.schemaDictionary(), 274 | String.t() 275 | ) :: [{:ok, ElmEncoders.product_encoder()} | {:error, PrinterError.t()}] 276 | defp to_encoder_property( 277 | type_def, 278 | schema_def, 279 | schema_dict, 280 | module_name 281 | ) do 282 | type_def.properties 283 | |> Enum.map(fn {child_name, child_path} -> 284 | with {:ok, {child_type_def, child_schema_def}} <- 285 | Resolver.resolve_type( 286 | child_path, 287 | type_def.path, 288 | schema_def, 289 | schema_dict 290 | ), 291 | {:ok, property_encoder} <- 292 | ElmEncoders.create_encoder_property( 293 | child_type_def, 294 | child_schema_def, 295 | child_name, 296 | [], 297 | schema_def, 298 | module_name 299 | ) do 300 | property_encoder = 301 | if type_def.name != :anonymous do 302 | %{ 303 | property_encoder 304 | | location: type_def.name <> "." <> property_encoder.name 305 | } 306 | else 307 | property_encoder 308 | end 309 | 310 | {:ok, property_encoder} 311 | else 312 | {:error, error} -> 313 | {:error, error} 314 | end 315 | end) 316 | end 317 | 318 | # Fuzzer 319 | 320 | @fuzzer_location Path.join(@templates_location, "fuzzers/product_fuzzer.elm.eex") 321 | EEx.function_from_file(:defp, :fuzzer_template, @fuzzer_location, [:product_fuzzer]) 322 | 323 | @impl JS2E.Printer.PrinterBehaviour 324 | @spec print_fuzzer( 325 | Types.typeDefinition(), 326 | SchemaDefinition.t(), 327 | Types.schemaDictionary(), 328 | String.t() 329 | ) :: PrinterResult.t() 330 | def print_fuzzer( 331 | %AnyOfType{name: name, path: path, types: type_paths}, 332 | schema_def, 333 | schema_dict, 334 | module_name 335 | ) do 336 | type_name = Naming.create_root_name(name, schema_def) 337 | argument_name = Naming.normalize_identifier(type_name, :downcase) 338 | decoder_name = "#{Naming.normalize_identifier(type_name, :downcase)}Decoder" 339 | encoder_name = "encode#{Naming.normalize_identifier(type_name, :upcase)}" 340 | fuzzer_name = "#{Naming.normalize_identifier(type_name, :downcase)}Fuzzer" 341 | 342 | {field_fuzzers, errors} = 343 | type_paths 344 | |> Enum.map( 345 | &create_property_fuzzer( 346 | &1, 347 | path, 348 | schema_def, 349 | schema_dict, 350 | module_name 351 | ) 352 | ) 353 | |> CommonOperations.split_ok_and_errors() 354 | 355 | field_fuzzers = field_fuzzers |> List.flatten() |> Enum.sort_by(& &1.field_name) 356 | 357 | %{ 358 | name: fuzzer_name, 359 | type: type_name, 360 | argument_name: argument_name, 361 | decoder_name: decoder_name, 362 | encoder_name: encoder_name, 363 | fuzzers: field_fuzzers 364 | } 365 | |> fuzzer_template() 366 | |> PrinterResult.new(errors) 367 | end 368 | 369 | @spec create_property_fuzzer( 370 | Types.typeIdentifier(), 371 | Types.typeIdentifier(), 372 | SchemaDefinition.t(), 373 | Types.schemaDictionary(), 374 | String.t() 375 | ) :: {:ok, ElmFuzzers.field_fuzzer()} | {:error, PrinterError.t() | ParserError.t()} 376 | defp create_property_fuzzer( 377 | type_path, 378 | parent, 379 | schema_def, 380 | schema_dict, 381 | module_name 382 | ) do 383 | with {:ok, {resolved_type, resolved_schema}} <- 384 | Resolver.resolve_type( 385 | type_path, 386 | parent, 387 | schema_def, 388 | schema_dict 389 | ), 390 | {:ok, fuzzers} <- 391 | ElmFuzzers.create_fuzzer_names( 392 | resolved_type.name, 393 | resolved_type, 394 | resolved_schema, 395 | schema_def, 396 | schema_dict, 397 | module_name 398 | ) do 399 | fuzzers = 400 | fuzzers 401 | |> Enum.map(fn fuzzer -> 402 | %{fuzzer | fuzzer_name: "(Fuzz.maybe #{fuzzer.fuzzer_name})"} 403 | end) 404 | 405 | {:ok, fuzzers} 406 | else 407 | {:error, error} -> 408 | {:error, error} 409 | end 410 | end 411 | end 412 | -------------------------------------------------------------------------------- /lib/printer/array_printer.ex: -------------------------------------------------------------------------------- 1 | defmodule JS2E.Printer.ArrayPrinter do 2 | @behaviour JS2E.Printer.PrinterBehaviour 3 | @moduledoc """ 4 | A printer for printing an 'array' type decoder. 5 | """ 6 | 7 | require Elixir.{EEx, Logger} 8 | alias JS2E.Printer 9 | alias JsonSchema.{Parser, Resolver, Types} 10 | alias Parser.ParserError 11 | alias Printer.{PrinterError, PrinterResult, Utils} 12 | 13 | alias Types.{ArrayType, PrimitiveType, SchemaDefinition} 14 | 15 | alias Utils.{ 16 | ElmDecoders, 17 | ElmEncoders, 18 | ElmFuzzers, 19 | ElmTypes, 20 | Naming 21 | } 22 | 23 | @templates_location Application.compile_env(:js2e, :templates_location) 24 | 25 | # Type 26 | 27 | @impl JS2E.Printer.PrinterBehaviour 28 | @spec print_type( 29 | Types.typeDefinition(), 30 | SchemaDefinition.t(), 31 | Types.schemaDictionary(), 32 | String.t() 33 | ) :: PrinterResult.t() 34 | def print_type( 35 | %ArrayType{name: _name, path: _path, items: _items_path}, 36 | _schema_def, 37 | _schema_dict, 38 | _module_name 39 | ) do 40 | PrinterResult.new("") 41 | end 42 | 43 | # Decoder 44 | 45 | @decoder_location Path.join(@templates_location, "decoders/list_decoder.elm.eex") 46 | EEx.function_from_file(:defp, :decoder_template, @decoder_location, [ 47 | :list_decoder 48 | ]) 49 | 50 | @impl JS2E.Printer.PrinterBehaviour 51 | @spec print_decoder( 52 | Types.typeDefinition(), 53 | SchemaDefinition.t(), 54 | Types.schemaDictionary(), 55 | String.t() 56 | ) :: PrinterResult.t() 57 | def print_decoder( 58 | %ArrayType{name: name, path: path, items: items_path}, 59 | schema_def, 60 | schema_dict, 61 | _module_name 62 | ) do 63 | with {:ok, {items_type, _resolved_schema_def}} <- 64 | Resolver.resolve_type(items_path, path, schema_def, schema_dict), 65 | {:ok, items_type_name} <- determine_type_name(items_type), 66 | {:ok, items_decoder_name} <- determine_decoder_name(items_type) do 67 | %{ 68 | name: "#{Naming.normalize_identifier(name, :downcase)}Decoder", 69 | type: items_type_name, 70 | item_decoder: items_decoder_name 71 | } 72 | |> decoder_template() 73 | |> PrinterResult.new() 74 | else 75 | {:error, %ParserError{identifier: id, error_type: atom, message: str}} -> 76 | PrinterResult.new("", [PrinterError.new(id, atom, str)]) 77 | 78 | {:error, printer_error} -> 79 | PrinterResult.new("", [printer_error]) 80 | end 81 | end 82 | 83 | @spec determine_type_name(Types.typeDefinition()) :: 84 | {:ok, String.t()} | {:error, PrinterError.t()} 85 | defp determine_type_name(items_type) do 86 | case items_type do 87 | %PrimitiveType{} -> 88 | {:ok, ElmTypes.determine_primitive_type_name(items_type.type)} 89 | 90 | _ -> 91 | items_type_name = Naming.normalize_identifier(items_type.name, :upcase) 92 | 93 | if items_type_name == "Hash" do 94 | {:ok, "Root"} 95 | else 96 | {:ok, items_type_name} 97 | end 98 | end 99 | end 100 | 101 | @spec determine_decoder_name(Types.typeDefinition()) :: 102 | {:ok, String.t()} | {:error, PrinterError.t()} 103 | defp determine_decoder_name(items_type) do 104 | case items_type do 105 | %PrimitiveType{} -> 106 | {:ok, ElmDecoders.determine_primitive_type_decoder(items_type.type)} 107 | 108 | _ -> 109 | items_type_name = Naming.normalize_identifier(items_type.name, :downcase) 110 | 111 | if items_type_name == "hash" do 112 | {:ok, "rootDecoder"} 113 | else 114 | {:ok, "#{items_type_name}Decoder"} 115 | end 116 | end 117 | end 118 | 119 | # Encoder 120 | 121 | @encoder_location Path.join(@templates_location, "encoders/list_encoder.elm.eex") 122 | EEx.function_from_file(:defp, :encoder_template, @encoder_location, [ 123 | :list_encoder 124 | ]) 125 | 126 | @impl JS2E.Printer.PrinterBehaviour 127 | @spec print_encoder( 128 | Types.typeDefinition(), 129 | SchemaDefinition.t(), 130 | Types.schemaDictionary(), 131 | String.t() 132 | ) :: PrinterResult.t() 133 | def print_encoder( 134 | %ArrayType{name: name, path: path, items: items_path}, 135 | schema_def, 136 | schema_dict, 137 | _module_name 138 | ) do 139 | with {:ok, {items_type, _resolved_schema_def}} <- 140 | Resolver.resolve_type(items_path, path, schema_def, schema_dict), 141 | {:ok, items_type_name} <- determine_type_name(items_type), 142 | {:ok, items_encoder_name} <- determine_encoder_name(items_type) do 143 | %{ 144 | name: "encode#{Naming.normalize_identifier(name, :upcase)}", 145 | type: items_type_name, 146 | argument_name: name, 147 | items_encoder: items_encoder_name 148 | } 149 | |> encoder_template() 150 | |> PrinterResult.new() 151 | else 152 | {:error, %ParserError{identifier: id, error_type: atom, message: str}} -> 153 | PrinterResult.new("", [PrinterError.new(id, atom, str)]) 154 | 155 | {:error, printer_error} -> 156 | PrinterResult.new("", [printer_error]) 157 | end 158 | end 159 | 160 | @spec determine_encoder_name(Types.typeDefinition()) :: 161 | {:ok, String.t()} | {:error, PrinterError.t()} 162 | defp determine_encoder_name(items_type) do 163 | case items_type do 164 | %PrimitiveType{} -> 165 | {:ok, ElmEncoders.determine_primitive_type_encoder(items_type.type)} 166 | 167 | _ -> 168 | items_type_name = Naming.normalize_identifier(items_type.name, :upcase) 169 | 170 | if items_type_name == "Hash" do 171 | {:ok, "encodeRoot"} 172 | else 173 | {:ok, "encode#{items_type_name}"} 174 | end 175 | end 176 | end 177 | 178 | # Fuzzer 179 | 180 | @fuzzer_location Path.join(@templates_location, "fuzzers/list_fuzzer.elm.eex") 181 | EEx.function_from_file(:defp, :fuzzer_template, @fuzzer_location, [ 182 | :list_fuzzer 183 | ]) 184 | 185 | @impl JS2E.Printer.PrinterBehaviour 186 | @spec print_fuzzer( 187 | Types.typeDefinition(), 188 | SchemaDefinition.t(), 189 | Types.schemaDictionary(), 190 | String.t() 191 | ) :: PrinterResult.t() 192 | def print_fuzzer( 193 | %ArrayType{name: name, path: path, items: items_path}, 194 | schema_def, 195 | schema_dict, 196 | _module_name 197 | ) do 198 | with {:ok, {items_type, _resolved_schema_def}} <- 199 | Resolver.resolve_type(items_path, path, schema_def, schema_dict), 200 | {:ok, items_type_name} <- determine_type_name(items_type), 201 | {:ok, items_fuzzer_name} <- determine_fuzzer_name(items_type) do 202 | array_name = Naming.normalize_identifier(name, :upcase) 203 | argument_name = Naming.normalize_identifier(name, :downcase) 204 | fuzzer_name = "#{name}Fuzzer" 205 | decoder_name = "#{Naming.normalize_identifier(name, :downcase)}Decoder" 206 | encoder_name = "encode#{Naming.normalize_identifier(name, :upcase)}" 207 | 208 | %{ 209 | name: fuzzer_name, 210 | array_name: array_name, 211 | items_type: items_type_name, 212 | items_fuzzer: items_fuzzer_name, 213 | argument_name: argument_name, 214 | decoder_name: decoder_name, 215 | encoder_name: encoder_name 216 | } 217 | |> fuzzer_template() 218 | |> PrinterResult.new() 219 | else 220 | {:error, %ParserError{identifier: id, error_type: atom, message: str}} -> 221 | PrinterResult.new("", [PrinterError.new(id, atom, str)]) 222 | 223 | {:error, printer_error} -> 224 | PrinterResult.new("", [printer_error]) 225 | end 226 | end 227 | 228 | @spec determine_fuzzer_name(Types.typeDefinition()) :: 229 | {:ok, String.t()} | {:error, PrinterError.t()} 230 | defp determine_fuzzer_name(items_type) do 231 | case items_type do 232 | %PrimitiveType{} -> 233 | {:ok, ElmFuzzers.determine_primitive_fuzzer_name(items_type.type)} 234 | 235 | _ -> 236 | items_type_name = Naming.normalize_identifier(items_type.name, :downcase) 237 | 238 | if items_type_name == "hash" do 239 | {:ok, "rootFuzzer"} 240 | else 241 | {:ok, "#{items_type_name}Fuzzer"} 242 | end 243 | end 244 | end 245 | end 246 | -------------------------------------------------------------------------------- /lib/printer/enum_printer.ex: -------------------------------------------------------------------------------- 1 | defmodule JS2E.Printer.EnumPrinter do 2 | @behaviour JS2E.Printer.PrinterBehaviour 3 | @moduledoc """ 4 | Prints the Elm type, JSON decoder and JSON eecoder for a JSON schema 'enum'. 5 | """ 6 | 7 | require Elixir.{EEx, Logger} 8 | alias JS2E.Printer 9 | alias JsonSchema.Types 10 | alias Printer.{PrinterResult, Utils} 11 | alias Types.{EnumType, SchemaDefinition} 12 | alias Utils.{Indentation, Naming} 13 | 14 | @templates_location Application.compile_env(:js2e, :templates_location) 15 | 16 | # Type 17 | 18 | @type_location Path.join(@templates_location, "types/sum_type.elm.eex") 19 | EEx.function_from_file(:defp, :type_template, @type_location, [:sum_type]) 20 | 21 | @impl JS2E.Printer.PrinterBehaviour 22 | @spec print_type( 23 | Types.typeDefinition(), 24 | SchemaDefinition.t(), 25 | Types.schemaDictionary(), 26 | String.t() 27 | ) :: PrinterResult.t() 28 | def print_type( 29 | %EnumType{name: name, path: _path, type: type, values: values}, 30 | _schema_def, 31 | _schema_dict, 32 | _module_name 33 | ) do 34 | clauses = 35 | values 36 | |> Enum.map(&create_elm_value(&1, type)) 37 | 38 | name = 39 | name 40 | |> Naming.normalize_identifier(:upcase) 41 | 42 | %{name: name, clauses: {:anonymous, clauses}} 43 | |> type_template() 44 | |> PrinterResult.new() 45 | end 46 | 47 | # Decoder 48 | 49 | @decoder_location Path.join(@templates_location, "decoders/enum_decoder.elm.eex") 50 | EEx.function_from_file(:defp, :decoder_template, @decoder_location, [:enum_decoder]) 51 | 52 | @impl JS2E.Printer.PrinterBehaviour 53 | @spec print_decoder( 54 | Types.typeDefinition(), 55 | SchemaDefinition.t(), 56 | Types.schemaDictionary(), 57 | String.t() 58 | ) :: PrinterResult.t() 59 | def print_decoder( 60 | %EnumType{name: name, path: _path, type: type, values: values}, 61 | _schema_def, 62 | _schema_dict, 63 | _module_name 64 | ) do 65 | decoder_name = "#{Naming.normalize_identifier(name, :downcase)}Decoder" 66 | decoder_type = Naming.upcase_first(name) 67 | argument_type = to_elm_type_name(type) 68 | parser_name = "parse#{Naming.normalize_identifier(name, :upcase)}" 69 | 70 | clauses = 71 | values 72 | |> create_decoder_clauses(type) 73 | 74 | %{ 75 | name: decoder_name, 76 | type: decoder_type, 77 | argument_name: name, 78 | argument_type: argument_type, 79 | parser_name: parser_name, 80 | clauses: clauses 81 | } 82 | |> decoder_template() 83 | |> PrinterResult.new() 84 | end 85 | 86 | @spec to_elm_type_name(EnumType.value_type_name()) :: String.t() 87 | defp to_elm_type_name(type_name) do 88 | case type_name do 89 | :string -> "string" 90 | :integer -> "int" 91 | :number -> "float" 92 | end 93 | end 94 | 95 | @type decoder_clause :: %{raw_value: EnumType.value_type(), parsed_value: String.t()} 96 | 97 | @spec create_decoder_clauses([EnumType.value_type()], EnumType.value_type_name()) :: [ 98 | decoder_clause() 99 | ] 100 | defp create_decoder_clauses(values, type_name) do 101 | values 102 | |> Enum.map(fn value -> 103 | raw_value = create_decoder_case(value, type_name) 104 | parsed_value = create_elm_value(value, type_name) 105 | %{raw_value: raw_value, parsed_value: parsed_value} 106 | end) 107 | end 108 | 109 | @spec create_decoder_case(EnumType.value_type(), EnumType.value_type_name()) :: 110 | EnumType.value_type() 111 | defp create_decoder_case(value, type_name) do 112 | case type_name do 113 | :string -> "\"#{value}\"" 114 | :integer -> value 115 | :number -> value 116 | end 117 | end 118 | 119 | # Encoder 120 | 121 | @encoder_location Path.join(@templates_location, "encoders/enum_encoder.elm.eex") 122 | EEx.function_from_file(:defp, :encoder_template, @encoder_location, [ 123 | :enum_encoder 124 | ]) 125 | 126 | @impl JS2E.Printer.PrinterBehaviour 127 | @spec print_encoder( 128 | Types.typeDefinition(), 129 | SchemaDefinition.t(), 130 | Types.schemaDictionary(), 131 | String.t() 132 | ) :: PrinterResult.t() 133 | def print_encoder( 134 | %EnumType{name: name, path: _path, type: type, values: values}, 135 | _schema_def, 136 | _schema_dict, 137 | _module_name 138 | ) do 139 | argument_type = Naming.normalize_identifier(name, :upcase) 140 | encoder_name = "encode#{argument_type}" 141 | argument_js_type = to_elm_type_name(type) |> String.capitalize() 142 | 143 | clauses = 144 | values 145 | |> create_encoder_cases(type) 146 | 147 | %{ 148 | name: encoder_name, 149 | type: argument_type, 150 | argument_name: name, 151 | argument_js_type: argument_js_type, 152 | clauses: clauses 153 | } 154 | |> encoder_template() 155 | |> Indentation.trim_newlines() 156 | |> PrinterResult.new() 157 | end 158 | 159 | @type encoder_case :: %{elm_value: String.t(), json_value: String.t()} 160 | 161 | @spec create_encoder_cases([EnumType.value_type()], EnumType.value_type_name()) :: [ 162 | encoder_case() 163 | ] 164 | defp create_encoder_cases(values, type_name) do 165 | values 166 | |> Enum.map(fn value -> 167 | elm_value = create_elm_value(value, type_name) 168 | json_value = create_encoder_case(value, type_name) 169 | %{elm_value: elm_value, json_value: json_value} 170 | end) 171 | end 172 | 173 | @spec create_encoder_case(EnumType.value_type(), EnumType.value_type_name()) :: String.t() 174 | defp create_encoder_case(value, type_name) do 175 | case type_name do 176 | :string -> "\"#{value}\"" 177 | :integer -> "#{value}" 178 | :number -> "#{value}" 179 | end 180 | end 181 | 182 | @spec create_elm_value(EnumType.value_type(), EnumType.value_type_name()) :: String.t() 183 | defp create_elm_value(value, type_name) do 184 | case type_name do 185 | :string -> 186 | Naming.normalize_identifier(value, :upcase) 187 | 188 | :integer -> 189 | "Int#{value}" 190 | 191 | :number -> 192 | "Float#{value}" 193 | |> String.replace(".", "_") 194 | |> String.replace("-", "Neg") 195 | end 196 | end 197 | 198 | # Fuzzer 199 | 200 | @fuzzer_location Path.join(@templates_location, "fuzzers/sum_fuzzer.elm.eex") 201 | EEx.function_from_file(:defp, :fuzzer_template, @fuzzer_location, [:sum_fuzzer]) 202 | 203 | @impl JS2E.Printer.PrinterBehaviour 204 | @spec print_fuzzer( 205 | Types.typeDefinition(), 206 | SchemaDefinition.t(), 207 | Types.schemaDictionary(), 208 | String.t() 209 | ) :: PrinterResult.t() 210 | def print_fuzzer( 211 | %EnumType{name: name, path: _path, type: _type, values: values}, 212 | schema_def, 213 | _schema_dict, 214 | _module_name 215 | ) do 216 | type_name = Naming.create_root_name(name, schema_def) 217 | argument_name = Naming.normalize_identifier(type_name, :downcase) 218 | decoder_name = "#{Naming.normalize_identifier(type_name, :downcase)}Decoder" 219 | encoder_name = "encode#{Naming.normalize_identifier(type_name, :upcase)}" 220 | fuzzer_name = "#{Naming.normalize_identifier(type_name, :downcase)}Fuzzer" 221 | 222 | fuzzers = 223 | values 224 | |> Enum.map(fn clause_fuzzer -> 225 | "Fuzz.constant #{Naming.normalize_identifier(clause_fuzzer, :upcase)}" 226 | end) 227 | 228 | %{ 229 | name: fuzzer_name, 230 | type: type_name, 231 | argument_name: argument_name, 232 | encoder_name: encoder_name, 233 | decoder_name: decoder_name, 234 | clause_fuzzers: List.flatten(fuzzers) 235 | } 236 | |> fuzzer_template() 237 | |> PrinterResult.new() 238 | end 239 | end 240 | -------------------------------------------------------------------------------- /lib/printer/error_util.ex: -------------------------------------------------------------------------------- 1 | defmodule JS2E.Printer.ErrorUtil do 2 | @moduledoc """ 3 | Contains helper functions for reporting printer errors. 4 | """ 5 | 6 | alias JS2E.Printer 7 | alias JsonSchema.Types 8 | alias Printer.PrinterError 9 | 10 | @spec unresolved_reference( 11 | Types.typeIdentifier(), 12 | URI.t() 13 | ) :: PrinterError.t() 14 | def unresolved_reference(identifier, parent) do 15 | printed_path = to_string(parent) 16 | stringified_value = sanitize_value(identifier) 17 | 18 | error_msg = """ 19 | 20 | The following reference at `#{printed_path}` could not be resolved 21 | 22 | "$ref": #{stringified_value} 23 | #{error_markings(stringified_value)} 24 | 25 | Be aware that newer versions of JSON schema uses "$id" rather than "id" when 26 | specifying the id of a JSON schema or subschema. 27 | 28 | Hint: See the specification section 8. "Base URI and Dereferencing" 29 | 30 | """ 31 | 32 | PrinterError.new(parent, :unresolved_reference, error_msg) 33 | end 34 | 35 | @spec unknown_type(String.t()) :: PrinterError.t() 36 | def unknown_type(type_name) do 37 | error_msg = "Could not find printer for type: '#{type_name}'" 38 | PrinterError.new(type_name, :unknown_type, error_msg) 39 | end 40 | 41 | @spec unexpected_type(Types.typeIdentifier(), String.t()) :: PrinterError.t() 42 | def unexpected_type(identifier, error_msg) do 43 | PrinterError.new(identifier, :unexpected_type, error_msg) 44 | end 45 | 46 | @spec unknown_enum_type(String.t()) :: PrinterError.t() 47 | def unknown_enum_type(type_name) do 48 | error_msg = "Unknown or unsupported enum type: '#{type_name}'" 49 | PrinterError.new(type_name, :unknown_enum_type, error_msg) 50 | end 51 | 52 | @spec unknown_primitive_type(String.t()) :: PrinterError.t() 53 | def unknown_primitive_type(type_name) do 54 | error_msg = "Unknown or unsupported primitive type: '#{type_name}'" 55 | PrinterError.new(type_name, :unknown_primitive_type, error_msg) 56 | end 57 | 58 | @spec name_collision(String.t()) :: PrinterError.t() 59 | def name_collision(file_name) do 60 | error_msg = "Found more than one schema with file: '#{file_name}'" 61 | PrinterError.new(file_name, :name_collision, error_msg) 62 | end 63 | 64 | @spec sanitize_value(any) :: String.t() 65 | defp sanitize_value(raw_value) do 66 | cond do 67 | is_map(raw_value) and raw_value.__struct__ == URI -> 68 | URI.to_string(raw_value) 69 | 70 | is_map(raw_value) -> 71 | "#{inspect(raw_value)}" 72 | 73 | true -> 74 | inspect(raw_value) 75 | end 76 | end 77 | 78 | @spec error_markings(String.t()) :: [String.t()] 79 | defp error_markings(value) do 80 | red(String.duplicate("^", String.length(value))) 81 | end 82 | 83 | @spec red(String.t()) :: [String.t()] 84 | defp red(str) do 85 | IO.ANSI.format([:red, str]) 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/printer/one_of_printer.ex: -------------------------------------------------------------------------------- 1 | defmodule JS2E.Printer.OneOfPrinter do 2 | @behaviour JS2E.Printer.PrinterBehaviour 3 | @moduledoc """ 4 | A printer for printing a 'one of' type decoder. 5 | """ 6 | 7 | require Elixir.{EEx, Logger} 8 | alias JS2E.Printer 9 | alias JsonSchema.{Parser, Resolver, Types} 10 | alias Parser.ParserError 11 | alias Printer.{PrinterError, PrinterResult, Utils} 12 | alias Types.{OneOfType, SchemaDefinition} 13 | 14 | alias Utils.{ 15 | CommonOperations, 16 | ElmDecoders, 17 | ElmFuzzers, 18 | ElmTypes, 19 | Indentation, 20 | Naming 21 | } 22 | 23 | @templates_location Application.compile_env(:js2e, :templates_location) 24 | 25 | # Type 26 | 27 | @type_location Path.join(@templates_location, "types/sum_type.elm.eex") 28 | EEx.function_from_file(:defp, :type_template, @type_location, [ 29 | :sum_type 30 | ]) 31 | 32 | @impl JS2E.Printer.PrinterBehaviour 33 | @spec print_type( 34 | Types.typeDefinition(), 35 | SchemaDefinition.t(), 36 | Types.schemaDictionary(), 37 | String.t() 38 | ) :: PrinterResult.t() 39 | def print_type( 40 | %OneOfType{name: name, path: path, types: types}, 41 | schema_def, 42 | schema_dict, 43 | _module_name 44 | ) do 45 | type_name = Naming.normalize_identifier(name, :upcase) 46 | 47 | {type_clauses, errors} = 48 | types 49 | |> create_type_clauses(name, path, schema_def, schema_dict) 50 | |> CommonOperations.split_ok_and_errors() 51 | 52 | %{name: type_name, clauses: {:named, type_clauses}} 53 | |> type_template() 54 | |> PrinterResult.new(errors) 55 | end 56 | 57 | @spec create_type_clauses( 58 | [URI.t()], 59 | String.t(), 60 | URI.t(), 61 | SchemaDefinition.t(), 62 | Types.schemaDictionary() 63 | ) :: [{:ok, ElmTypes.named_clause()} | {:error, PrinterError.t() | ParserError.t()}] 64 | defp create_type_clauses(type_clauses, name, parent, schema_def, schema_dict) do 65 | type_clauses 66 | |> Enum.map(&create_type_clause(&1, name, parent, schema_def, schema_dict)) 67 | end 68 | 69 | @spec create_type_clause( 70 | URI.t(), 71 | String.t(), 72 | URI.t(), 73 | SchemaDefinition.t(), 74 | Types.schemaDictionary() 75 | ) :: {:ok, ElmTypes.named_clause()} | {:error, PrinterError.t() | ParserError.t()} 76 | defp create_type_clause(type_clause_id, name, parent, schema_def, schema_dict) do 77 | case Resolver.resolve_type( 78 | type_clause_id, 79 | parent, 80 | schema_def, 81 | schema_dict 82 | ) do 83 | {:ok, {type_clause, _resolved_schema_def}} -> 84 | type_value = Naming.normalize_identifier(type_clause.name, :upcase) 85 | 86 | type_prefix = 87 | type_value 88 | |> String.slice(0..1) 89 | |> String.capitalize() 90 | 91 | type_name = Naming.normalize_identifier(name, :upcase) 92 | 93 | {:ok, %{name: "#{type_name}#{type_prefix}", type: type_value}} 94 | 95 | {:error, error} -> 96 | {:error, error} 97 | end 98 | end 99 | 100 | # Decoder 101 | 102 | @decoder_location Path.join(@templates_location, "decoders/sum_decoder.elm.eex") 103 | EEx.function_from_file(:defp, :decoder_template, @decoder_location, [ 104 | :sum_decoder 105 | ]) 106 | 107 | @impl JS2E.Printer.PrinterBehaviour 108 | @spec print_decoder( 109 | Types.typeDefinition(), 110 | SchemaDefinition.t(), 111 | Types.schemaDictionary(), 112 | String.t() 113 | ) :: PrinterResult.t() 114 | def print_decoder( 115 | %OneOfType{name: name, path: path, types: types}, 116 | schema_def, 117 | schema_dict, 118 | _module_name 119 | ) do 120 | {clause_decoders, errors} = 121 | types 122 | |> create_decoder_clauses(name, path, schema_def, schema_dict) 123 | |> CommonOperations.split_ok_and_errors() 124 | 125 | normalized_name = Naming.normalize_identifier(name, :downcase) 126 | decoder_name = "#{normalized_name}Decoder" 127 | decoder_type = Naming.upcase_first(normalized_name) 128 | 129 | %{name: decoder_name, type: decoder_type, optional: false, clauses: {:named, clause_decoders}} 130 | |> decoder_template() 131 | |> PrinterResult.new(errors) 132 | end 133 | 134 | @spec create_decoder_clauses( 135 | [URI.t()], 136 | String.t(), 137 | URI.t(), 138 | SchemaDefinition.t(), 139 | Types.schemaDictionary() 140 | ) :: [{:ok, ElmDecoders.named_sum_clause()} | {:error, PrinterError.t()}] 141 | defp create_decoder_clauses( 142 | type_clauses, 143 | name, 144 | parent, 145 | schema_def, 146 | schema_dict 147 | ) do 148 | type_clauses 149 | |> Enum.map(&create_decoder_clause(&1, name, parent, schema_def, schema_dict)) 150 | end 151 | 152 | @spec create_decoder_clause( 153 | URI.t(), 154 | String.t(), 155 | URI.t(), 156 | SchemaDefinition.t(), 157 | Types.schemaDictionary() 158 | ) :: {:ok, ElmDecoders.named_sum_clause()} | {:error, PrinterError.t() | ParserError.t()} 159 | defp create_decoder_clause( 160 | type_clause_id, 161 | name, 162 | parent, 163 | schema_def, 164 | schema_dict 165 | ) do 166 | case Resolver.resolve_type( 167 | type_clause_id, 168 | parent, 169 | schema_def, 170 | schema_dict 171 | ) do 172 | {:ok, {type_clause, _resolved_schema_def}} -> 173 | type_prefix = 174 | type_clause.name 175 | |> Naming.normalize_identifier(:upcase) 176 | |> String.slice(0..1) 177 | |> String.capitalize() 178 | 179 | constructor_name = "#{Naming.normalize_identifier(name, :upcase)}#{type_prefix}" 180 | {:ok, %{decoder_name: "#{type_clause.name}Decoder", constructor_name: constructor_name}} 181 | 182 | {:error, error} -> 183 | {:error, error} 184 | end 185 | end 186 | 187 | # Encoder 188 | 189 | @encoder_location Path.join(@templates_location, "encoders/sum_encoder.elm.eex") 190 | EEx.function_from_file(:defp, :encoder_template, @encoder_location, [:sum_encoder]) 191 | 192 | @impl JS2E.Printer.PrinterBehaviour 193 | @spec print_encoder( 194 | Types.typeDefinition(), 195 | SchemaDefinition.t(), 196 | Types.schemaDictionary(), 197 | String.t() 198 | ) :: PrinterResult.t() 199 | def print_encoder( 200 | %OneOfType{name: name, path: path, types: types}, 201 | schema_def, 202 | schema_dict, 203 | _module_name 204 | ) do 205 | {encoder_cases, errors} = 206 | types 207 | |> create_encoder_cases(name, path, schema_def, schema_dict) 208 | |> CommonOperations.split_ok_and_errors() 209 | 210 | type_name = Naming.normalize_identifier(name, :upcase) 211 | encoder_name = "encode#{type_name}" 212 | 213 | %{name: encoder_name, type: type_name, argument_name: name, cases: encoder_cases} 214 | |> encoder_template() 215 | |> Indentation.trim_newlines() 216 | |> PrinterResult.new(errors) 217 | end 218 | 219 | @spec create_encoder_cases( 220 | [String.t()], 221 | String.t(), 222 | URI.t(), 223 | SchemaDefinition.t(), 224 | Types.schemaDictionary() 225 | ) :: [{:ok, encoder_clause()} | {:error, PrinterError.t() | ParserError.t()}] 226 | defp create_encoder_cases(types, name, parent, schema_def, schema_dict) do 227 | types 228 | |> Enum.map(&create_encoder_clause(&1, name, parent, schema_def, schema_dict)) 229 | end 230 | 231 | @type encoder_clause :: %{constructor: String.t(), encoder: String.t()} 232 | 233 | @spec create_encoder_clause( 234 | String.t(), 235 | String.t(), 236 | URI.t(), 237 | SchemaDefinition.t(), 238 | Types.schemaDictionary() 239 | ) :: {:ok, encoder_clause()} | {:error, PrinterError.t() | ParserError.t()} 240 | defp create_encoder_clause(type_path, name, parent, schema_def, schema_dict) do 241 | case Resolver.resolve_type(type_path, parent, schema_def, schema_dict) do 242 | {:ok, {clause_type, _resolved_schema_def}} -> 243 | type_name = Naming.normalize_identifier(name, :upcase) 244 | argument_name = Naming.normalize_identifier(clause_type.name, :downcase) 245 | type_value = Naming.upcase_first(argument_name) 246 | 247 | type_prefix = 248 | type_value 249 | |> String.slice(0..1) 250 | |> String.capitalize() 251 | 252 | {:ok, 253 | %{ 254 | constructor: "#{type_name}#{type_prefix} #{argument_name}", 255 | encoder: "encode#{type_value} #{argument_name}" 256 | }} 257 | 258 | {:error, error} -> 259 | {:error, error} 260 | end 261 | end 262 | 263 | # Fuzzer 264 | 265 | @fuzzer_location Path.join(@templates_location, "fuzzers/sum_fuzzer.elm.eex") 266 | EEx.function_from_file(:defp, :fuzzer_template, @fuzzer_location, [:sum_fuzzer]) 267 | 268 | @impl JS2E.Printer.PrinterBehaviour 269 | @spec print_fuzzer( 270 | Types.typeDefinition(), 271 | SchemaDefinition.t(), 272 | Types.schemaDictionary(), 273 | String.t() 274 | ) :: PrinterResult.t() 275 | def print_fuzzer( 276 | %OneOfType{name: name, path: path, types: types}, 277 | schema_def, 278 | schema_dict, 279 | module_name 280 | ) do 281 | type_name = Naming.create_root_name(name, schema_def) 282 | argument_name = Naming.normalize_identifier(type_name, :downcase) 283 | decoder_name = "#{Naming.normalize_identifier(type_name, :downcase)}Decoder" 284 | encoder_name = "encode#{Naming.normalize_identifier(type_name, :upcase)}" 285 | fuzzer_name = "#{Naming.normalize_identifier(type_name, :downcase)}Fuzzer" 286 | 287 | {fuzzers, errors} = 288 | types 289 | |> create_fuzzer_properties( 290 | path, 291 | schema_def, 292 | schema_dict, 293 | module_name 294 | ) 295 | |> CommonOperations.split_ok_and_errors() 296 | 297 | %{ 298 | name: fuzzer_name, 299 | type: type_name, 300 | argument_name: argument_name, 301 | encoder_name: encoder_name, 302 | decoder_name: decoder_name, 303 | clause_fuzzers: List.flatten(fuzzers) 304 | } 305 | |> fuzzer_template() 306 | |> PrinterResult.new(errors) 307 | end 308 | 309 | @spec create_fuzzer_properties( 310 | [URI.t()], 311 | URI.t(), 312 | SchemaDefinition.t(), 313 | Types.schemaDictionary(), 314 | String.t() 315 | ) :: [{:ok, ElmFuzzers.field_fuzzer()} | {:error, PrinterError.t() | ParserError.t()}] 316 | defp create_fuzzer_properties( 317 | types, 318 | parent, 319 | schema_def, 320 | schema_dict, 321 | module_name 322 | ) do 323 | types 324 | |> Enum.map( 325 | &create_fuzzer_property( 326 | &1, 327 | parent, 328 | schema_def, 329 | schema_dict, 330 | module_name 331 | ) 332 | ) 333 | end 334 | 335 | @spec create_fuzzer_property( 336 | URI.t(), 337 | URI.t(), 338 | SchemaDefinition.t(), 339 | Types.schemaDictionary(), 340 | String.t() 341 | ) :: {:ok, [ElmFuzzers.field_fuzzer()]} | {:error, PrinterError.t() | ParserError.t()} 342 | defp create_fuzzer_property( 343 | type, 344 | parent, 345 | schema_def, 346 | schema_dict, 347 | module_name 348 | ) do 349 | with {:ok, {resolved_type, resolved_schema}} <- 350 | Resolver.resolve_type( 351 | type, 352 | parent, 353 | schema_def, 354 | schema_dict 355 | ), 356 | {:ok, fuzzer_names} <- 357 | ElmFuzzers.create_fuzzer_names( 358 | resolved_type.name, 359 | resolved_type, 360 | resolved_schema, 361 | schema_def, 362 | schema_dict, 363 | module_name 364 | ) do 365 | {:ok, Enum.map(fuzzer_names, fn result -> result.fuzzer_name end)} 366 | else 367 | {:error, error} -> 368 | {:error, error} 369 | end 370 | end 371 | end 372 | -------------------------------------------------------------------------------- /lib/printer/preamble_printer.ex: -------------------------------------------------------------------------------- 1 | defmodule JS2E.Printer.PreamblePrinter do 2 | @moduledoc """ 3 | A printer for printing a 'preamble' for a module. 4 | """ 5 | 6 | require Elixir.{EEx, Logger} 7 | alias JS2E.Printer 8 | alias JsonSchema.Types 9 | alias Printer.{PrinterResult, Utils} 10 | alias Types.{SchemaDefinition, TypeReference} 11 | alias Utils.Naming 12 | 13 | @templates_location Application.compile_env(:js2e, :templates_location) 14 | 15 | @preamble_location Path.join( 16 | @templates_location, 17 | "preamble/preamble.elm.eex" 18 | ) 19 | 20 | EEx.function_from_file(:defp, :preamble_template, @preamble_location, [ 21 | :prefix, 22 | :title, 23 | :description, 24 | :imports 25 | ]) 26 | 27 | @spec print_preamble( 28 | SchemaDefinition.t(), 29 | Types.schemaDictionary(), 30 | String.t() 31 | ) :: PrinterResult.t() 32 | def print_preamble( 33 | %SchemaDefinition{ 34 | id: _id, 35 | title: title, 36 | description: description, 37 | types: _type_dict 38 | } = schema_def, 39 | schema_dict, 40 | module_name 41 | ) do 42 | imports = create_imports(schema_def, schema_dict) 43 | file_name = Naming.normalize_identifier(title, :upcase) 44 | 45 | module_name 46 | |> create_prefix() 47 | |> preamble_template(file_name, description, imports) 48 | |> PrinterResult.new() 49 | end 50 | 51 | @tests_preamble_location Path.join( 52 | @templates_location, 53 | "preamble/tests_preamble.elm.eex" 54 | ) 55 | 56 | EEx.function_from_file( 57 | :defp, 58 | :tests_preamble_template, 59 | @tests_preamble_location, 60 | [ 61 | :prefix, 62 | :title, 63 | :description, 64 | :imports 65 | ] 66 | ) 67 | 68 | @spec print_tests_preamble( 69 | SchemaDefinition.t(), 70 | Types.schemaDictionary(), 71 | String.t() 72 | ) :: PrinterResult.t() 73 | def print_tests_preamble( 74 | %SchemaDefinition{ 75 | id: _id, 76 | title: title, 77 | description: description, 78 | types: _type_dict 79 | } = schema_def, 80 | schema_dict, 81 | module_name 82 | ) do 83 | imports = create_imports(schema_def, schema_dict) 84 | file_name = Naming.normalize_identifier(title, :upcase) 85 | 86 | module_name 87 | |> create_prefix() 88 | |> tests_preamble_template(file_name, description, imports) 89 | |> PrinterResult.new() 90 | end 91 | 92 | @spec create_prefix(String.t()) :: String.t() 93 | defp create_prefix(module_name) do 94 | if module_name != "" do 95 | module_name <> "." 96 | else 97 | "" 98 | end 99 | end 100 | 101 | @spec create_imports( 102 | SchemaDefinition.t(), 103 | Types.schemaDictionary() 104 | ) :: [String.t()] 105 | defp create_imports(schema_def, schema_dict) do 106 | schema_id = schema_def.id 107 | type_dict = schema_def.types 108 | 109 | type_dict 110 | |> get_type_references 111 | |> create_dependency_map(schema_id, schema_dict) 112 | |> create_dependencies(schema_def, schema_dict) 113 | end 114 | 115 | @spec get_type_references(Types.typeDictionary()) :: [TypeReference.t()] 116 | defp get_type_references(type_dict) do 117 | type_dict 118 | |> Enum.reduce([], fn {_path, type}, types -> 119 | case type do 120 | %TypeReference{} -> 121 | [type | types] 122 | 123 | _ -> 124 | types 125 | end 126 | end) 127 | end 128 | 129 | @spec create_dependency_map( 130 | [TypeReference.t()], 131 | URI.t(), 132 | Types.schemaDictionary() 133 | ) :: %{required(String.t()) => TypeReference.t()} 134 | defp create_dependency_map(type_refs, schema_id, schema_dict) do 135 | type_refs 136 | |> Enum.reduce(%{}, fn type_ref, dependency_map -> 137 | type_ref 138 | |> resolve_dependency(dependency_map, schema_id, schema_dict) 139 | end) 140 | end 141 | 142 | @spec resolve_dependency( 143 | TypeReference.t(), 144 | %{required(String.t()) => [TypeReference.t()]}, 145 | URI.t(), 146 | Types.schemaDictionary() 147 | ) :: %{required(String.t()) => [TypeReference.t()]} 148 | defp resolve_dependency(type_ref, dependency_map, schema_uri, schema_dict) do 149 | type_ref_uri = URI.parse(type_ref.path |> to_string) 150 | 151 | cond do 152 | has_relative_path?(type_ref_uri) -> 153 | dependency_map 154 | 155 | has_same_absolute_path?(type_ref_uri, schema_uri) -> 156 | dependency_map 157 | 158 | has_different_absolute_path?(type_ref_uri, schema_uri) -> 159 | type_ref_schema_uri = 160 | type_ref_uri 161 | |> Map.put(:fragment, nil) 162 | |> to_string 163 | 164 | type_ref_schema_def = schema_dict[type_ref_schema_uri] 165 | 166 | type_refs = 167 | if Map.has_key?(dependency_map, type_ref_schema_def.id) do 168 | [type_ref | dependency_map[type_ref_schema_def.id]] 169 | else 170 | [type_ref] 171 | end 172 | 173 | dependency_map 174 | |> Map.put(type_ref_schema_def.id, type_refs) 175 | 176 | true -> 177 | Logger.error("Could not resolve #{inspect(type_ref)}") 178 | Logger.error("with type_ref_uri #{to_string(type_ref_uri)}") 179 | Logger.error("In dependency map #{inspect(dependency_map)}") 180 | Logger.error("Where schema_uri #{inspect(schema_uri)}") 181 | Logger.error("and schema_dict #{inspect(schema_dict)}") 182 | dependency_map 183 | end 184 | end 185 | 186 | @spec create_dependencies( 187 | %{required(String.t()) => TypeReference.t()}, 188 | SchemaDefinition.t(), 189 | Types.schemaDictionary() 190 | ) :: [String.t()] 191 | defp create_dependencies(dependency_map, _schema_def, schema_dict) do 192 | dependency_map 193 | |> Enum.map(fn {schema_id, _type_refs} -> 194 | type_ref_schema = schema_dict[to_string(schema_id)] 195 | type_ref_schema.title 196 | end) 197 | end 198 | 199 | @spec has_relative_path?(URI.t()) :: boolean 200 | defp has_relative_path?(type_uri) do 201 | type_uri.scheme == nil 202 | end 203 | 204 | @spec has_same_absolute_path?(URI.t(), URI.t()) :: boolean 205 | defp has_same_absolute_path?(type_uri, schema_uri) do 206 | type_uri.host == schema_uri.host and type_uri.path == schema_uri.path 207 | end 208 | 209 | @spec has_different_absolute_path?(URI.t(), URI.t()) :: boolean 210 | defp has_different_absolute_path?(type_uri, schema_uri) do 211 | type_uri.host == schema_uri.host and type_uri.path != schema_uri.path 212 | end 213 | end 214 | -------------------------------------------------------------------------------- /lib/printer/primitive_printer.ex: -------------------------------------------------------------------------------- 1 | defmodule JS2E.Printer.PrimitivePrinter do 2 | @behaviour JS2E.Printer.PrinterBehaviour 3 | @moduledoc """ 4 | A printer for printing an 'object' type decoder. 5 | """ 6 | 7 | require Logger 8 | alias JS2E.Printer.PrinterResult 9 | alias JsonSchema.Types 10 | alias Types.{PrimitiveType, SchemaDefinition} 11 | 12 | @impl JS2E.Printer.PrinterBehaviour 13 | @spec print_type( 14 | Types.typeDefinition(), 15 | SchemaDefinition.t(), 16 | Types.schemaDictionary(), 17 | String.t() 18 | ) :: PrinterResult.t() 19 | def print_type( 20 | %PrimitiveType{name: _name, path: _path, type: _type}, 21 | _schema_def, 22 | _schema_dict, 23 | _module_name 24 | ) do 25 | PrinterResult.new("") 26 | end 27 | 28 | @impl JS2E.Printer.PrinterBehaviour 29 | @spec print_decoder( 30 | Types.typeDefinition(), 31 | SchemaDefinition.t(), 32 | Types.schemaDictionary(), 33 | String.t() 34 | ) :: PrinterResult.t() 35 | def print_decoder( 36 | %PrimitiveType{name: _name, path: _path, type: _type}, 37 | _schema_def, 38 | _schema_dict, 39 | _module_name 40 | ) do 41 | PrinterResult.new("") 42 | end 43 | 44 | @impl JS2E.Printer.PrinterBehaviour 45 | @spec print_encoder( 46 | Types.typeDefinition(), 47 | SchemaDefinition.t(), 48 | Types.schemaDictionary(), 49 | String.t() 50 | ) :: PrinterResult.t() 51 | def print_encoder( 52 | %PrimitiveType{name: _name, path: _path, type: _type}, 53 | _schema_def, 54 | _schema_dict, 55 | _module_name 56 | ) do 57 | PrinterResult.new("") 58 | end 59 | 60 | @impl JS2E.Printer.PrinterBehaviour 61 | @spec print_fuzzer( 62 | Types.typeDefinition(), 63 | SchemaDefinition.t(), 64 | Types.schemaDictionary(), 65 | String.t() 66 | ) :: PrinterResult.t() 67 | def print_fuzzer( 68 | %PrimitiveType{name: _name, path: _path, type: _type}, 69 | _schema_def, 70 | _schema_dict, 71 | _module_name 72 | ) do 73 | PrinterResult.new("") 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/printer/printer_behaviour.ex: -------------------------------------------------------------------------------- 1 | defmodule JS2E.Printer.PrinterBehaviour do 2 | @moduledoc """ 3 | Describes the functions needed to implement a printer of a JSON schema node. 4 | """ 5 | 6 | alias JS2E.Printer.PrinterResult 7 | alias JsonSchema.Types 8 | alias Types.SchemaDefinition 9 | 10 | @callback print_type( 11 | Types.typeDefinition(), 12 | SchemaDefinition.t(), 13 | Types.schemaDictionary(), 14 | String.t() 15 | ) :: PrinterResult.t() 16 | 17 | @callback print_decoder( 18 | Types.typeDefinition(), 19 | SchemaDefinition.t(), 20 | Types.schemaDictionary(), 21 | String.t() 22 | ) :: PrinterResult.t() 23 | 24 | @callback print_encoder( 25 | Types.typeDefinition(), 26 | SchemaDefinition.t(), 27 | Types.schemaDictionary(), 28 | String.t() 29 | ) :: PrinterResult.t() 30 | 31 | @callback print_fuzzer( 32 | Types.typeDefinition(), 33 | SchemaDefinition.t(), 34 | Types.schemaDictionary(), 35 | String.t() 36 | ) :: PrinterResult.t() 37 | end 38 | -------------------------------------------------------------------------------- /lib/printer/printer_result_types.ex: -------------------------------------------------------------------------------- 1 | defmodule JS2E.Printer.PrinterError do 2 | @moduledoc """ 3 | Represents an error generated while printing a JSON schema object 4 | as Elm code. 5 | """ 6 | 7 | use TypedStruct 8 | alias JsonSchema.Types 9 | 10 | @type error_type :: 11 | :unresolved_reference 12 | | :unknown_type 13 | | :unexpected_type 14 | | :unknown_enum_type 15 | | :unknown_primitive_type 16 | | :name_collision 17 | 18 | typedstruct do 19 | field(:identifier, Types.typeIdentifier(), enforce: true) 20 | field(:error_type, error_type, enforce: true) 21 | field(:message, String.t(), enforce: true) 22 | end 23 | 24 | @doc """ 25 | Constructs a `PrinterError`. 26 | """ 27 | @spec new(Types.typeIdentifier(), error_type, String.t()) :: t 28 | def new(identifier, error_type, message) do 29 | %__MODULE__{ 30 | identifier: identifier, 31 | error_type: error_type, 32 | message: message 33 | } 34 | end 35 | end 36 | 37 | defmodule JS2E.Printer.PrinterResult do 38 | @moduledoc """ 39 | Represents the result of printing a subset of a JSON schema as Elm code 40 | including printed schema, warnings, and errors. 41 | """ 42 | 43 | require Logger 44 | alias JS2E.Printer.PrinterError 45 | 46 | @type t :: %__MODULE__{printed_schema: String.t(), errors: [PrinterError.t()]} 47 | 48 | defstruct [:printed_schema, :errors] 49 | 50 | @doc """ 51 | Returns an empty `PrinterResult`. 52 | """ 53 | @spec new :: t 54 | def new, do: %__MODULE__{printed_schema: "", errors: []} 55 | 56 | @doc """ 57 | Creates a `PrinterResult`. 58 | """ 59 | @spec new(String.t(), [PrinterError.t()]) :: t 60 | def new(printed_schema, errors \\ []) do 61 | %__MODULE__{printed_schema: printed_schema, errors: errors} 62 | end 63 | 64 | @doc """ 65 | Merges two `PrinterResult`s and adds any errors from merging their file 66 | dictionaries to the list of errors in the merged `PrinterResult`. 67 | 68 | """ 69 | @spec merge(t, t) :: t 70 | def merge(result1, result2) do 71 | merged_schema = 72 | String.trim(result1.printed_schema) <> 73 | "\n\n\n" <> String.trim(result2.printed_schema) 74 | 75 | merged_errors = Enum.uniq(result1.errors ++ result2.errors) 76 | 77 | new(merged_schema, merged_errors) 78 | end 79 | end 80 | 81 | defmodule JS2E.Printer.SchemaResult do 82 | @moduledoc """ 83 | Represents the result of printing a whole JSON schema document as Elm code 84 | including printed schema, warnings, and errors. 85 | """ 86 | 87 | require Logger 88 | alias JS2E.Printer.PrinterError 89 | 90 | @type fileDictionary :: %{required(Path.t()) => String.t()} 91 | 92 | @type t :: %__MODULE__{ 93 | file_dict: fileDictionary(), 94 | errors: [{Path.t(), PrinterError.t()}] 95 | } 96 | 97 | defstruct [:file_dict, :errors] 98 | 99 | @doc """ 100 | Returns an empty `SchemaResult`. 101 | """ 102 | @spec new :: t 103 | def new, do: %__MODULE__{file_dict: %{}, errors: []} 104 | 105 | @doc """ 106 | Creates a `SchemaResult`. 107 | """ 108 | @spec new(fileDictionary(), [PrinterError.t()]) :: t 109 | def new(file_dict, errors \\ []) do 110 | %__MODULE__{file_dict: file_dict, errors: errors} 111 | end 112 | 113 | @doc """ 114 | Merges two `SchemaResult`s and adds any errors from merging their file 115 | dictionaries to the list of errors in the merged `SchemaResult`. 116 | 117 | """ 118 | @spec merge(t, t) :: t 119 | def merge(result1, result2) do 120 | merged_file_dict = Map.merge(result1.file_dict, result2.file_dict) 121 | merged_errors = result1.errors ++ result2.errors 122 | 123 | %__MODULE__{file_dict: merged_file_dict, errors: merged_errors} 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /lib/printer/tuple_printer.ex: -------------------------------------------------------------------------------- 1 | defmodule JS2E.Printer.TuplePrinter do 2 | @behaviour JS2E.Printer.PrinterBehaviour 3 | @moduledoc """ 4 | A printer for printing a 'tuple' type decoder. 5 | """ 6 | 7 | require Elixir.{EEx, Logger} 8 | alias JS2E.Printer 9 | alias JsonSchema.{Parser, Resolver, Types} 10 | alias Parser.ParserError 11 | alias Printer.{PrinterError, PrinterResult, Utils} 12 | 13 | alias Utils.{ 14 | CommonOperations, 15 | ElmDecoders, 16 | ElmEncoders, 17 | ElmFuzzers, 18 | ElmTypes, 19 | Indentation, 20 | Naming 21 | } 22 | 23 | alias Types.{SchemaDefinition, TupleType} 24 | 25 | @templates_location Application.compile_env(:js2e, :templates_location) 26 | 27 | # Type 28 | 29 | @type_location Path.join(@templates_location, "types/product_type.elm.eex") 30 | EEx.function_from_file(:defp, :type_template, @type_location, [ 31 | :product_type 32 | ]) 33 | 34 | @impl JS2E.Printer.PrinterBehaviour 35 | @spec print_type( 36 | Types.typeDefinition(), 37 | SchemaDefinition.t(), 38 | Types.schemaDictionary(), 39 | String.t() 40 | ) :: PrinterResult.t() 41 | def print_type( 42 | %TupleType{name: name, path: path, items: types}, 43 | schema_def, 44 | schema_dict, 45 | module_name 46 | ) do 47 | {type_fields, errors} = 48 | types 49 | |> create_type_fields(path, schema_def, schema_dict, module_name) 50 | |> CommonOperations.split_ok_and_errors() 51 | 52 | type_name = name |> Naming.normalize_identifier(:upcase) 53 | 54 | %{name: type_name, fields: {:anonymous, List.flatten(type_fields)}} 55 | |> type_template() 56 | |> PrinterResult.new(errors) 57 | end 58 | 59 | @spec create_type_fields( 60 | [URI.t()], 61 | URI.t(), 62 | SchemaDefinition.t(), 63 | Types.schemaDictionary(), 64 | String.t() 65 | ) :: [{:ok, [ElmTypes.named_field()]} | {:error, PrinterError.t()}] 66 | defp create_type_fields(types, parent, schema_def, schema_dict, module_name) do 67 | types 68 | |> Enum.map(&create_type_field(&1, parent, schema_def, schema_dict, module_name)) 69 | end 70 | 71 | @spec create_type_field( 72 | URI.t(), 73 | URI.t(), 74 | SchemaDefinition.t(), 75 | Types.schemaDictionary(), 76 | String.t() 77 | ) :: {:ok, [ElmTypes.named_field()]} | {:error, PrinterError.t()} 78 | defp create_type_field( 79 | type_path, 80 | parent, 81 | schema_def, 82 | schema_dict, 83 | module_name 84 | ) do 85 | with {:ok, {resolved_type, resolved_schema}} <- 86 | Resolver.resolve_type(type_path, parent, schema_def, schema_dict), 87 | {:ok, type_names} <- 88 | ElmTypes.create_fields( 89 | :anonymous, 90 | resolved_type, 91 | resolved_schema, 92 | parent, 93 | schema_def, 94 | schema_dict, 95 | module_name 96 | ) do 97 | {:ok, type_names} 98 | else 99 | {:error, error} -> 100 | {:error, error} 101 | end 102 | end 103 | 104 | # Decoder 105 | 106 | @decoder_location Path.join(@templates_location, "decoders/product_decoder.elm.eex") 107 | EEx.function_from_file(:defp, :decoder_template, @decoder_location, [ 108 | :product_decoder 109 | ]) 110 | 111 | @impl JS2E.Printer.PrinterBehaviour 112 | @spec print_decoder( 113 | Types.typeDefinition(), 114 | SchemaDefinition.t(), 115 | Types.schemaDictionary(), 116 | String.t() 117 | ) :: PrinterResult.t() 118 | def print_decoder( 119 | %TupleType{name: name, path: path, items: type_paths}, 120 | schema_def, 121 | schema_dict, 122 | module_name 123 | ) do 124 | {decoder_clauses, errors} = 125 | type_paths 126 | |> create_decoder_clauses(path, schema_def, schema_dict, module_name) 127 | |> CommonOperations.split_ok_and_errors() 128 | 129 | normalized_name = Naming.normalize_identifier(name, :downcase) 130 | decoder_name = "#{normalized_name}Decoder" 131 | type_name = Naming.upcase_first(normalized_name) 132 | 133 | %{name: decoder_name, type: type_name, clauses: {:anonymous, List.flatten(decoder_clauses)}} 134 | |> decoder_template() 135 | |> PrinterResult.new(errors) 136 | end 137 | 138 | @spec create_decoder_clauses( 139 | [URI.t()], 140 | URI.t(), 141 | SchemaDefinition.t(), 142 | Types.schemaDictionary(), 143 | String.t() 144 | ) :: [{:ok, ElmDecoders.named_product_clause()} | {:error, PrinterError.t()}] 145 | defp create_decoder_clauses( 146 | type_paths, 147 | parent, 148 | schema_def, 149 | schema_dict, 150 | module_name 151 | ) do 152 | type_paths 153 | |> Enum.map(&create_decoder_clause(&1, parent, schema_def, schema_dict, module_name)) 154 | end 155 | 156 | @spec create_decoder_clause( 157 | Types.typeIdentifier(), 158 | Types.typeIdentifier(), 159 | SchemaDefinition.t(), 160 | Types.schemaDictionary(), 161 | String.t() 162 | ) :: 163 | {:ok, ElmDecoders.named_product_clause()} 164 | | {:error, PrinterError.t() | ParserError.t()} 165 | defp create_decoder_clause( 166 | type_path, 167 | parent, 168 | schema_def, 169 | schema_dict, 170 | module_name 171 | ) do 172 | with {:ok, {property_type, resolved_schema_def}} <- 173 | Resolver.resolve_type(type_path, parent, schema_def, schema_dict), 174 | {:ok, clauses} <- 175 | ElmDecoders.create_clauses( 176 | :anonymous, 177 | property_type, 178 | resolved_schema_def, 179 | [], 180 | schema_def, 181 | schema_dict, 182 | module_name 183 | ) do 184 | {:ok, clauses} 185 | else 186 | {:error, error} -> 187 | {:error, error} 188 | end 189 | end 190 | 191 | # Encoder 192 | 193 | @encoder_location Path.join(@templates_location, "encoders/tuple_encoder.elm.eex") 194 | EEx.function_from_file(:defp, :encoder_template, @encoder_location, [ 195 | :tuple_encoder 196 | ]) 197 | 198 | @impl JS2E.Printer.PrinterBehaviour 199 | @spec print_encoder( 200 | Types.typeDefinition(), 201 | SchemaDefinition.t(), 202 | Types.schemaDictionary(), 203 | String.t() 204 | ) :: PrinterResult.t() 205 | def print_encoder( 206 | %TupleType{name: name, path: path, items: type_paths}, 207 | schema_def, 208 | schema_dict, 209 | module_name 210 | ) do 211 | {encoder_properties, errors} = 212 | type_paths 213 | |> create_encoder_properties(path, schema_def, schema_dict, module_name) 214 | |> CommonOperations.split_ok_and_errors() 215 | 216 | type_name = Naming.normalize_identifier(name, :upcase) 217 | encoder_name = "encode#{Naming.normalize_identifier(type_name, :upcase)}" 218 | 219 | %{name: encoder_name, type: type_name, properties: encoder_properties} 220 | |> encoder_template() 221 | |> Indentation.trim_newlines() 222 | |> PrinterResult.new(errors) 223 | end 224 | 225 | @spec create_encoder_properties( 226 | [URI.t()], 227 | URI.t(), 228 | SchemaDefinition.t(), 229 | Types.schemaDictionary(), 230 | String.t() 231 | ) :: [{:ok, ElmEncoders.product_property()} | {:error, PrinterError.t()}] 232 | defp create_encoder_properties( 233 | type_paths, 234 | parent, 235 | schema_def, 236 | schema_dict, 237 | module_name 238 | ) do 239 | type_paths 240 | |> Enum.map(&Resolver.resolve_type(&1, parent, schema_def, schema_dict)) 241 | |> Enum.map(&to_encoder_property(&1, schema_def, module_name)) 242 | end 243 | 244 | @spec to_encoder_property( 245 | {:ok, {Types.typeDefinition(), SchemaDefinition.t()}} 246 | | {:error, PrinterError.t()}, 247 | SchemaDefinition.t(), 248 | Types.schemaDictionary() 249 | ) :: {:ok, ElmEncoders.product_property()} | {:error, PrinterError.t()} 250 | defp to_encoder_property({:error, error}, _sd, _md), do: {:error, error} 251 | 252 | defp to_encoder_property( 253 | {:ok, {resolved_property, resolved_schema}}, 254 | schema_def, 255 | module_name 256 | ) do 257 | ElmEncoders.create_encoder_property( 258 | resolved_property, 259 | resolved_schema, 260 | resolved_property.name, 261 | [], 262 | schema_def, 263 | module_name 264 | ) 265 | end 266 | 267 | # Fuzzer 268 | 269 | @fuzzer_location Path.join(@templates_location, "fuzzers/tuple_fuzzer.elm.eex") 270 | EEx.function_from_file(:defp, :fuzzer_template, @fuzzer_location, [:tuple_fuzzer]) 271 | 272 | @impl JS2E.Printer.PrinterBehaviour 273 | @spec print_fuzzer( 274 | Types.typeDefinition(), 275 | SchemaDefinition.t(), 276 | Types.schemaDictionary(), 277 | String.t() 278 | ) :: PrinterResult.t() 279 | def print_fuzzer( 280 | %TupleType{name: name, path: path, items: items_paths}, 281 | schema_def, 282 | schema_dict, 283 | module_name 284 | ) do 285 | type_name = Naming.create_root_name(name, schema_def) 286 | argument_name = Naming.normalize_identifier(name, :downcase) 287 | fuzzer_name = "#{name}Fuzzer" 288 | decoder_name = "#{Naming.normalize_identifier(name, :downcase)}Decoder" 289 | encoder_name = "encode#{Naming.normalize_identifier(type_name, :upcase)}" 290 | 291 | {fuzzers, errors} = 292 | items_paths 293 | |> create_items_fuzzers( 294 | path, 295 | schema_def, 296 | schema_dict, 297 | module_name 298 | ) 299 | |> CommonOperations.split_ok_and_errors() 300 | 301 | %{ 302 | name: fuzzer_name, 303 | type: type_name, 304 | argument_name: argument_name, 305 | decoder_name: decoder_name, 306 | encoder_name: encoder_name, 307 | field_fuzzers: List.flatten(fuzzers) 308 | } 309 | |> fuzzer_template() 310 | |> PrinterResult.new(errors) 311 | end 312 | 313 | @spec create_items_fuzzers( 314 | [URI.t()], 315 | URI.t(), 316 | SchemaDefinition.t(), 317 | Types.schemaDictionary(), 318 | String.t() 319 | ) :: [{:ok, [ElmFuzzers.field_fuzzer()]} | {:error, PrinterError.t()}] 320 | defp create_items_fuzzers( 321 | items_paths, 322 | parent, 323 | schema_def, 324 | schema_dict, 325 | module_name 326 | ) do 327 | items_paths 328 | |> Enum.map( 329 | &create_item_fuzzer( 330 | &1, 331 | parent, 332 | schema_def, 333 | schema_dict, 334 | module_name 335 | ) 336 | ) 337 | end 338 | 339 | @spec create_item_fuzzer( 340 | URI.t(), 341 | URI.t(), 342 | SchemaDefinition.t(), 343 | Types.schemaDictionary(), 344 | String.t() 345 | ) :: {:ok, [ElmFuzzers.field_fuzzer()]} | {:error, PrinterError.t()} 346 | defp create_item_fuzzer( 347 | item_path, 348 | parent, 349 | schema_def, 350 | schema_dict, 351 | module_name 352 | ) do 353 | with {:ok, {resolved_type, resolved_schema}} <- 354 | Resolver.resolve_type( 355 | item_path, 356 | parent, 357 | schema_def, 358 | schema_dict 359 | ), 360 | {:ok, fuzzer_names} <- 361 | ElmFuzzers.create_fuzzer_names( 362 | resolved_type.name, 363 | resolved_type, 364 | resolved_schema, 365 | schema_def, 366 | schema_dict, 367 | module_name 368 | ) do 369 | {:ok, fuzzer_names} 370 | else 371 | {:error, error} -> 372 | {:error, error} 373 | end 374 | end 375 | end 376 | -------------------------------------------------------------------------------- /lib/printer/type_reference_printer.ex: -------------------------------------------------------------------------------- 1 | defmodule JS2E.Printer.TypeReferencePrinter do 2 | @behaviour JS2E.Printer.PrinterBehaviour 3 | @moduledoc ~S""" 4 | A printer for printing a type reference decoder. 5 | """ 6 | 7 | require Logger 8 | alias JS2E.Printer 9 | alias JsonSchema.Types 10 | alias Printer.PrinterResult 11 | alias Types.{SchemaDefinition, TypeReference} 12 | 13 | @impl JS2E.Printer.PrinterBehaviour 14 | @spec print_type( 15 | Types.typeDefinition(), 16 | SchemaDefinition.t(), 17 | Types.schemaDictionary(), 18 | String.t() 19 | ) :: PrinterResult.t() 20 | def print_type( 21 | %TypeReference{name: _name, path: _path}, 22 | _schema_def, 23 | _schema_dict, 24 | _module_name 25 | ) do 26 | PrinterResult.new("") 27 | end 28 | 29 | @impl JS2E.Printer.PrinterBehaviour 30 | @spec print_decoder( 31 | Types.typeDefinition(), 32 | SchemaDefinition.t(), 33 | Types.schemaDictionary(), 34 | String.t() 35 | ) :: PrinterResult.t() 36 | def print_decoder( 37 | %TypeReference{name: _name, path: _path}, 38 | _schema_def, 39 | _schema_dict, 40 | _module_name 41 | ) do 42 | PrinterResult.new("") 43 | end 44 | 45 | @impl JS2E.Printer.PrinterBehaviour 46 | @spec print_encoder( 47 | Types.typeDefinition(), 48 | SchemaDefinition.t(), 49 | Types.schemaDictionary(), 50 | String.t() 51 | ) :: PrinterResult.t() 52 | def print_encoder( 53 | %TypeReference{name: _name, path: _path}, 54 | _schema_def, 55 | _schema_dict, 56 | _module_name 57 | ) do 58 | PrinterResult.new("") 59 | end 60 | 61 | @impl JS2E.Printer.PrinterBehaviour 62 | @spec print_fuzzer( 63 | Types.typeDefinition(), 64 | SchemaDefinition.t(), 65 | Types.schemaDictionary(), 66 | String.t() 67 | ) :: PrinterResult.t() 68 | def print_fuzzer( 69 | %TypeReference{name: _name, path: _path}, 70 | _schema_def, 71 | _schema_dict, 72 | _module_name 73 | ) do 74 | PrinterResult.new("") 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/printer/union_printer.ex: -------------------------------------------------------------------------------- 1 | defmodule JS2E.Printer.UnionPrinter do 2 | @behaviour JS2E.Printer.PrinterBehaviour 3 | @moduledoc """ 4 | A printer for printing an 'object' type decoder. 5 | """ 6 | 7 | require Elixir.{EEx, Logger} 8 | alias JS2E.Printer 9 | alias JsonSchema.Types 10 | alias Printer.{PrinterResult, Utils} 11 | alias Types.{SchemaDefinition, UnionType} 12 | alias Utils.{ElmTypes, Indentation, Naming} 13 | 14 | @templates_location Application.compile_env(:js2e, :templates_location) 15 | 16 | # Type 17 | 18 | @type_location Path.join(@templates_location, "types/sum_type.elm.eex") 19 | EEx.function_from_file(:defp, :type_template, @type_location, [ 20 | :sum_type 21 | ]) 22 | 23 | @impl JS2E.Printer.PrinterBehaviour 24 | @spec print_type( 25 | Types.typeDefinition(), 26 | SchemaDefinition.t(), 27 | Types.schemaDictionary(), 28 | String.t() 29 | ) :: PrinterResult.t() 30 | def print_type( 31 | %UnionType{name: name, path: _path, types: types}, 32 | _schema_def, 33 | _schema_dict, 34 | _module_name 35 | ) do 36 | type_clauses = 37 | types 38 | |> create_type_clauses(name) 39 | 40 | type_name = name |> Naming.normalize_identifier(:upcase) 41 | 42 | %{name: type_name, clauses: {:named, type_clauses}} 43 | |> type_template() 44 | |> PrinterResult.new() 45 | end 46 | 47 | @spec create_type_clauses([UnionType.value_type()], String.t()) :: [ElmTypes.named_clause()] 48 | defp create_type_clauses(value_type, name) do 49 | value_type 50 | |> Enum.filter(&(&1 != :null)) 51 | |> Enum.map(&to_type_clause(&1, name)) 52 | end 53 | 54 | @spec to_type_clause(UnionType.value_type(), String.t()) :: ElmTypes.named_clause() 55 | defp to_type_clause(value_type, name) do 56 | type_name = Naming.normalize_identifier(name, :upcase) 57 | 58 | case value_type do 59 | :boolean -> %{name: "#{type_name}_B", type: "Bool"} 60 | :integer -> %{name: "#{type_name}_I", type: "Int"} 61 | :number -> %{name: "#{type_name}_F", type: "Float"} 62 | :string -> %{name: "#{type_name}_S", type: "String"} 63 | end 64 | end 65 | 66 | # Decoder 67 | 68 | @decoder_location Path.join(@templates_location, "decoders/sum_decoder.elm.eex") 69 | EEx.function_from_file(:defp, :decoder_template, @decoder_location, [ 70 | :sum_decoder 71 | ]) 72 | 73 | @impl JS2E.Printer.PrinterBehaviour 74 | @spec print_decoder( 75 | Types.typeDefinition(), 76 | SchemaDefinition.t(), 77 | Types.schemaDictionary(), 78 | String.t() 79 | ) :: PrinterResult.t() 80 | def print_decoder( 81 | %UnionType{name: name, path: _path, types: types}, 82 | _schema_def, 83 | _schema_dict, 84 | _module_name 85 | ) do 86 | normalized_name = Naming.normalize_identifier(name, :downcase) 87 | decoder_name = "#{normalized_name}Decoder" 88 | type_name = Naming.upcase_first(normalized_name) 89 | optional = :null in types 90 | decoder_type = check_if_maybe(type_name, optional) 91 | decoder_clauses = types |> create_decoder_clauses(type_name, optional) 92 | 93 | %{ 94 | name: decoder_name, 95 | type: decoder_type, 96 | optional: optional, 97 | clauses: {:named, decoder_clauses} 98 | } 99 | |> decoder_template() 100 | |> PrinterResult.new() 101 | end 102 | 103 | @spec check_if_maybe(String.t(), boolean) :: String.t() 104 | defp check_if_maybe(type_name, optional) do 105 | if optional do 106 | "(Maybe #{type_name})" 107 | else 108 | type_name 109 | end 110 | end 111 | 112 | @spec create_decoder_clauses([UnionType.value_type()], String.t(), boolean) :: [ 113 | decoder_clause() 114 | ] 115 | defp create_decoder_clauses(value_type, type_name, optional) do 116 | value_type 117 | |> Enum.filter(fn type_id -> type_id != :null end) 118 | |> Enum.map(&create_decoder_clause(&1, type_name, optional)) 119 | end 120 | 121 | @type decoder_clause :: %{ 122 | decoder_name: String.t(), 123 | constructor_name: String.t() 124 | } 125 | 126 | @spec create_decoder_clause(UnionType.value_type(), String.t(), boolean) :: decoder_clause() 127 | defp create_decoder_clause(value_type, type_name, optional) do 128 | {constructor_suffix, decoder_name} = 129 | case value_type do 130 | :boolean -> {"_B", "Decode.bool"} 131 | :integer -> {"_I", "Decode.int"} 132 | :number -> {"_F", "Decode.float"} 133 | :string -> {"_S", "Decode.string"} 134 | end 135 | 136 | constructor_name = type_name <> constructor_suffix 137 | 138 | constructor_name = 139 | if optional do 140 | "(#{constructor_name} >> Just)" 141 | else 142 | constructor_name 143 | end 144 | 145 | %{ 146 | decoder_name: decoder_name, 147 | constructor_name: constructor_name 148 | } 149 | end 150 | 151 | # Encoder 152 | 153 | @encoder_location Path.join(@templates_location, "encoders/sum_encoder.elm.eex") 154 | EEx.function_from_file(:defp, :encoder_template, @encoder_location, [ 155 | :sum_encoder 156 | ]) 157 | 158 | @impl JS2E.Printer.PrinterBehaviour 159 | @spec print_encoder( 160 | Types.typeDefinition(), 161 | SchemaDefinition.t(), 162 | Types.schemaDictionary(), 163 | String.t() 164 | ) :: PrinterResult.t() 165 | def print_encoder( 166 | %UnionType{name: name, path: _path, types: types}, 167 | _schema_def, 168 | _schema_dict, 169 | _module_name 170 | ) do 171 | encoder_cases = 172 | types 173 | |> create_encoder_cases(name) 174 | 175 | type_name = Naming.normalize_identifier(name, :upcase) 176 | encoder_name = "encode#{type_name}" 177 | 178 | %{name: encoder_name, type: type_name, argument_name: name, cases: encoder_cases} 179 | |> encoder_template() 180 | |> Indentation.trim_newlines() 181 | |> PrinterResult.new() 182 | end 183 | 184 | @spec create_encoder_cases([UnionType.value_type()], String.t()) :: [encoder_case()] 185 | defp create_encoder_cases(value_types, name) do 186 | value_types 187 | |> Enum.map(&create_encoder_case(&1, name)) 188 | end 189 | 190 | @type encoder_case :: %{ 191 | constructor: String.t(), 192 | encoder: String.t() 193 | } 194 | 195 | @spec create_encoder_case(UnionType.value_type(), String.t()) :: encoder_case() 196 | defp create_encoder_case(value_type, name) do 197 | {constructor_suffix, encoder_name, argument_name} = 198 | case value_type do 199 | :boolean -> {"_B", "Encode.bool", "boolValue"} 200 | :integer -> {"_I", "Encode.int", "intValue"} 201 | :number -> {"_F", "Encode.float", "floatValue"} 202 | :string -> {"_S", "Encode.string", "stringValue"} 203 | end 204 | 205 | constructor_name = Naming.normalize_identifier(name, :upcase) <> constructor_suffix 206 | 207 | %{ 208 | constructor: "#{constructor_name} #{argument_name}", 209 | encoder: "#{encoder_name} #{argument_name}" 210 | } 211 | end 212 | 213 | # Fuzzer 214 | 215 | @fuzzer_location Path.join(@templates_location, "fuzzers/sum_fuzzer.elm.eex") 216 | EEx.function_from_file(:defp, :fuzzer_template, @fuzzer_location, [:sum_fuzzer]) 217 | 218 | @impl JS2E.Printer.PrinterBehaviour 219 | @spec print_fuzzer( 220 | Types.typeDefinition(), 221 | SchemaDefinition.t(), 222 | Types.schemaDictionary(), 223 | String.t() 224 | ) :: PrinterResult.t() 225 | def print_fuzzer( 226 | %UnionType{name: name, path: _path, types: types}, 227 | schema_def, 228 | _schema_dict, 229 | _module_name 230 | ) do 231 | type_name = Naming.create_root_name(name, schema_def) 232 | argument_name = Naming.normalize_identifier(type_name, :downcase) 233 | decoder_name = "#{Naming.normalize_identifier(type_name, :downcase)}Decoder" 234 | encoder_name = "encode#{Naming.normalize_identifier(type_name, :upcase)}" 235 | fuzzer_name = "#{Naming.normalize_identifier(type_name, :downcase)}Fuzzer" 236 | 237 | fuzzers = 238 | types 239 | |> Enum.map(fn value_type -> to_clause_fuzzer(value_type, type_name) end) 240 | 241 | %{ 242 | name: fuzzer_name, 243 | type: type_name, 244 | argument_name: argument_name, 245 | encoder_name: encoder_name, 246 | decoder_name: decoder_name, 247 | clause_fuzzers: List.flatten(fuzzers) 248 | } 249 | |> fuzzer_template() 250 | |> PrinterResult.new() 251 | end 252 | 253 | @type clause_fuzzer :: String.t() 254 | 255 | @spec to_clause_fuzzer(UnionType.value_type(), String.t()) :: clause_fuzzer() 256 | defp to_clause_fuzzer(value_type, type_name) do 257 | primitive_fuzzer = primitive_type_to_fuzzer(value_type) 258 | 259 | case value_type do 260 | :boolean -> "Fuzz.map #{type_name}_B #{primitive_fuzzer}" 261 | :integer -> "Fuzz.map #{type_name}_I #{primitive_fuzzer}" 262 | :number -> "Fuzz.map #{type_name}_F #{primitive_fuzzer}" 263 | :string -> "Fuzz.map #{type_name}_S #{primitive_fuzzer}" 264 | end 265 | end 266 | 267 | @spec primitive_type_to_fuzzer(UnionType.value_type()) :: clause_fuzzer() 268 | defp primitive_type_to_fuzzer(value_type) do 269 | case value_type do 270 | :boolean -> "Fuzz.bool" 271 | :integer -> "Fuzz.int" 272 | :number -> "Fuzz.niceFloat" 273 | :string -> "Fuzz.string" 274 | end 275 | end 276 | end 277 | -------------------------------------------------------------------------------- /lib/printer/utils/common_operations.ex: -------------------------------------------------------------------------------- 1 | defmodule JS2E.Printer.Utils.CommonOperations do 2 | @moduledoc ~S""" 3 | Module containing various utility functions 4 | for common operations across printers. 5 | """ 6 | 7 | require Logger 8 | alias JS2E.Printer.PrinterError 9 | 10 | @spec split_ok_and_errors([{:ok, any} | {:error, PrinterError.t()}]) :: 11 | {[any], [PrinterError.t()]} 12 | def split_ok_and_errors(results) do 13 | results 14 | |> Enum.reverse() 15 | |> Enum.reduce({[], []}, fn result, {oks, errors} -> 16 | case result do 17 | {:ok, ok} -> 18 | {[ok | oks], errors} 19 | 20 | {:error, error} -> 21 | {oks, [error | errors]} 22 | end 23 | end) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/printer/utils/elm_decoders.ex: -------------------------------------------------------------------------------- 1 | defmodule JS2E.Printer.Utils.ElmDecoders do 2 | @moduledoc ~S""" 3 | Module containing common utility functions for outputting 4 | Elm decoder definitions. 5 | """ 6 | 7 | require Logger 8 | alias JS2E.Printer 9 | alias JsonSchema.{Resolver, Types} 10 | alias Printer.{PrinterError, Utils} 11 | alias Types.{ObjectType, PrimitiveType, SchemaDefinition} 12 | alias Utils.{CommonOperations, Naming} 13 | 14 | @type decoder_definition :: 15 | {:product, product_decoder} 16 | | {:sum, sum_decoder} 17 | | {:enum, enum_decoder} 18 | | {:list, list_decoder} 19 | 20 | @type product_decoder :: 21 | %{ 22 | name: String.t(), 23 | type: String.t(), 24 | clauses: 25 | {:anonymous, [anonymous_product_clause]} 26 | | {:named, [named_product_clause]} 27 | } 28 | 29 | @type anonymous_product_clause :: %{decoder_name: String.t()} 30 | @type named_product_clause :: %{ 31 | option: option, 32 | property_name: String.t(), 33 | decoder_name: String.t() 34 | } 35 | 36 | @type option :: :required | :optional | :custom 37 | 38 | @type sum_decoder :: %{ 39 | name: String.t(), 40 | type: String.t(), 41 | optional: boolean, 42 | clauses: {:anonymous, [anonymous_sum_clause]} | {:named, [named_sum_clause]} 43 | } 44 | 45 | @type anonymous_sum_clause :: %{decoder_name: String.t()} 46 | @type named_sum_clause :: %{ 47 | decoder_name: String.t(), 48 | constructor_name: String.t() 49 | } 50 | 51 | @type enum_decoder :: %{ 52 | name: String.t(), 53 | type: String.t(), 54 | argument_name: String.t(), 55 | argument_type: String.t(), 56 | parser_name: String.t(), 57 | cases: [%{raw_value: String.t(), parsed_value: String.t()}] 58 | } 59 | 60 | @type list_decoder :: %{ 61 | name: String.t(), 62 | type: String.t(), 63 | item_decoder: String.t() 64 | } 65 | 66 | @spec create_clauses( 67 | String.t() | :anonymous, 68 | Types.typeDefinition(), 69 | SchemaDefinition.t(), 70 | [String.t()], 71 | SchemaDefinition.t(), 72 | Types.schemaDictionary(), 73 | String.t() 74 | ) :: {:ok, [named_product_clause()]} | {:error, PrinterError.t()} 75 | def create_clauses( 76 | property_name, 77 | resolved_type, 78 | resolved_schema, 79 | required, 80 | context_schema, 81 | schema_dict, 82 | module_name 83 | ) do 84 | if resolved_type.name == :anonymous do 85 | do_create_clauses( 86 | resolved_type, 87 | resolved_schema, 88 | required, 89 | context_schema, 90 | schema_dict, 91 | module_name 92 | ) 93 | else 94 | do_create_clause( 95 | property_name, 96 | resolved_type, 97 | resolved_schema, 98 | required, 99 | context_schema, 100 | schema_dict, 101 | module_name 102 | ) 103 | end 104 | end 105 | 106 | @spec do_create_clauses( 107 | Types.typeDefinition(), 108 | SchemaDefinition.t(), 109 | [String.t()], 110 | SchemaDefinition.t(), 111 | Types.schemaDictionary(), 112 | String.t() 113 | ) :: {:ok, [named_product_clause()]} | {:error, PrinterError.t()} 114 | def do_create_clauses( 115 | resolved_type, 116 | _resolved_schema, 117 | _required, 118 | context_schema, 119 | schema_dict, 120 | _module_name 121 | ) do 122 | case resolved_type do 123 | %ObjectType{} -> 124 | {decoder_names, errors} = 125 | resolved_type.properties 126 | |> Enum.map(fn {name, path} -> 127 | case Resolver.resolve_type( 128 | path, 129 | resolved_type.path, 130 | context_schema, 131 | schema_dict 132 | ) do 133 | {:ok, {property_type, _property_schema}} -> 134 | case property_type do 135 | %PrimitiveType{} -> 136 | decoder_name = determine_primitive_type_decoder(property_type.type) 137 | 138 | {:ok, 139 | %{ 140 | option: :required, 141 | property_name: name, 142 | decoder_name: "#{decoder_name}" 143 | }} 144 | 145 | _ -> 146 | {:ok, 147 | %{ 148 | option: :required, 149 | property_name: name, 150 | decoder_name: "#{Naming.downcase_first(property_type.name)}Decoder" 151 | }} 152 | end 153 | 154 | {:error, error} -> 155 | {:error, error} 156 | end 157 | end) 158 | |> CommonOperations.split_ok_and_errors() 159 | 160 | if errors != [] do 161 | {:error, errors} 162 | else 163 | {:ok, decoder_names} 164 | end 165 | 166 | _ -> 167 | {:ok, []} 168 | end 169 | end 170 | 171 | @spec do_create_clause( 172 | String.t() | :anonymous, 173 | Types.typeDefinition(), 174 | SchemaDefinition.t(), 175 | [String.t()], 176 | SchemaDefinition.t(), 177 | Types.schemaDictionary(), 178 | String.t() 179 | ) :: {:ok, [named_product_clause()]} | {:error, PrinterError.t()} 180 | def do_create_clause( 181 | maybe_property_name, 182 | resolved_type, 183 | resolved_schema, 184 | required, 185 | context_schema, 186 | _schema_dict, 187 | module_name 188 | ) do 189 | property_name = 190 | if maybe_property_name != :anonymous do 191 | maybe_property_name 192 | else 193 | resolved_type.name 194 | end 195 | 196 | option = 197 | if property_name in required do 198 | :required 199 | else 200 | :optional 201 | end 202 | 203 | case resolved_type do 204 | %PrimitiveType{} -> 205 | decoder_name = determine_primitive_type_decoder(resolved_type.type) 206 | 207 | {:ok, 208 | [ 209 | %{ 210 | option: option, 211 | property_name: property_name, 212 | decoder_name: decoder_name 213 | } 214 | ]} 215 | 216 | _ -> 217 | type_name = resolved_type.name |> Naming.normalize_identifier(:downcase) 218 | 219 | decoder_name = 220 | if type_name == "#" do 221 | if resolved_schema.title != nil do 222 | "#{Naming.downcase_first(resolved_schema.title)}Decoder" 223 | else 224 | "rootDecoder" 225 | end 226 | else 227 | "#{type_name}Decoder" 228 | end 229 | 230 | decoder_name = 231 | decoder_name 232 | |> check_qualified_name( 233 | resolved_schema, 234 | context_schema, 235 | module_name 236 | ) 237 | 238 | {:ok, 239 | [ 240 | %{ 241 | option: option, 242 | property_name: property_name, 243 | decoder_name: decoder_name 244 | } 245 | ]} 246 | end 247 | end 248 | 249 | @spec check_qualified_name(String.t(), SchemaDefinition.t(), SchemaDefinition.t(), String.t()) :: 250 | String.t() 251 | defp check_qualified_name(decoder_name, resolved_schema, context_schema, module_name) do 252 | if resolved_schema.id != context_schema.id do 253 | Naming.qualify_name( 254 | resolved_schema, 255 | decoder_name, 256 | module_name 257 | ) 258 | else 259 | decoder_name 260 | end 261 | end 262 | 263 | @doc ~S""" 264 | Converts a primitive value type into the corresponding Elm decoder. 265 | 266 | ## Examples 267 | 268 | iex> determine_primitive_type_decoder(:string) 269 | "Decode.string" 270 | 271 | iex> determine_primitive_type_decoder(:integer) 272 | "Decode.int" 273 | 274 | iex> determine_primitive_type_decoder(:number) 275 | "Decode.float" 276 | 277 | iex> determine_primitive_type_decoder(:boolean) 278 | "Decode.bool" 279 | """ 280 | @spec determine_primitive_type_decoder(PrimitiveType.value_type()) :: String.t() 281 | def determine_primitive_type_decoder(value_type) do 282 | case value_type do 283 | :string -> "Decode.string" 284 | :integer -> "Decode.int" 285 | :number -> "Decode.float" 286 | :boolean -> "Decode.bool" 287 | :null -> "(Decode.null ())" 288 | end 289 | end 290 | end 291 | -------------------------------------------------------------------------------- /lib/printer/utils/elm_encoders.ex: -------------------------------------------------------------------------------- 1 | defmodule JS2E.Printer.Utils.ElmEncoders do 2 | @moduledoc """ 3 | Module containing common utility functions for outputting 4 | Elm encoder definitions. 5 | """ 6 | 7 | require Logger 8 | alias JS2E.Printer 9 | alias JsonSchema.Types 10 | alias Printer.{PrinterError, Utils} 11 | alias Types.{PrimitiveType, SchemaDefinition} 12 | alias Utils.Naming 13 | 14 | @type encoder_definition :: 15 | {:product, product_encoder} 16 | | {:tuple, tuple_encoder} 17 | | {:sum, sum_encoder} 18 | | {:enum, enum_encoder} 19 | | {:list, list_encoder} 20 | 21 | @type product_encoder :: %{ 22 | name: String.t(), 23 | type: String.t(), 24 | argument_name: String.t(), 25 | properties: [product_property] 26 | } 27 | 28 | @type product_property :: %{ 29 | required: boolean, 30 | name: String.t(), 31 | location: String.t(), 32 | encoder_name: String.t() 33 | } 34 | 35 | @type tuple_encoder :: %{ 36 | name: String.t(), 37 | type: String.t(), 38 | properties: [%{name: String.t(), encoder_name: String.t()}] 39 | } 40 | 41 | @type sum_encoder :: %{ 42 | name: String.t(), 43 | type: String.t(), 44 | argument_name: String.t(), 45 | cases: %{constructor: String.t(), encoder: String.t()} 46 | } 47 | 48 | @type enum_encoder :: %{ 49 | name: String.t(), 50 | type: String.t(), 51 | argument_name: String.t(), 52 | argument_js_type: String.t(), 53 | clauses: [%{elm_value: String.t(), json_value: String.t()}] 54 | } 55 | 56 | @type list_encoder :: %{ 57 | name: String.t(), 58 | type: String.t(), 59 | argument_name: String.t(), 60 | items_encoder: String.t() 61 | } 62 | 63 | @doc """ 64 | Returns the encoder name given a JSON schema type definition. 65 | """ 66 | @spec create_encoder_property( 67 | Types.typeDefinition(), 68 | SchemaDefinition.t(), 69 | String.t(), 70 | [String.t()], 71 | SchemaDefinition.t(), 72 | String.t() 73 | ) :: {:ok, product_property()} | {:error, PrinterError.t()} 74 | def create_encoder_property( 75 | resolved_type, 76 | resolved_schema, 77 | property_name, 78 | required, 79 | context_schema, 80 | module_name 81 | ) do 82 | encoder_name = 83 | case resolved_type do 84 | %PrimitiveType{} -> 85 | determine_primitive_type_encoder(resolved_type.type) 86 | 87 | _ -> 88 | type_name = resolved_type.name |> Naming.normalize_identifier(:upcase) 89 | 90 | if type_name == "#" do 91 | if resolved_schema.title != nil do 92 | "encode#{Naming.upcase_first(resolved_schema.title)}" 93 | else 94 | "encodeRoot" 95 | end 96 | else 97 | "encode#{Naming.upcase_first(type_name)}" 98 | end 99 | end 100 | 101 | if resolved_schema.id != context_schema.id do 102 | {:ok, 103 | %{ 104 | name: Naming.normalize_identifier(property_name, :downcase), 105 | location: Naming.normalize_identifier(property_name, :downcase), 106 | encoder_name: Naming.qualify_name(resolved_schema, encoder_name, module_name), 107 | required: property_name in required 108 | }} 109 | else 110 | {:ok, 111 | %{ 112 | name: Naming.normalize_identifier(property_name, :downcase), 113 | location: Naming.normalize_identifier(property_name, :downcase), 114 | encoder_name: encoder_name, 115 | required: property_name in required 116 | }} 117 | end 118 | end 119 | 120 | @doc ~S""" 121 | Converts a primitive value type into the corresponding Elm encoder. 122 | 123 | ## Examples 124 | 125 | iex> determine_primitive_type_encoder(:string) 126 | "Encode.string" 127 | 128 | iex> determine_primitive_type_encoder(:integer) 129 | "Encode.int" 130 | 131 | iex> determine_primitive_type_encoder(:number) 132 | "Encode.float" 133 | 134 | iex> determine_primitive_type_encoder(:boolean) 135 | "Encode.bool" 136 | 137 | iex> determine_primitive_type_encoder(:null) 138 | "Encode.null" 139 | """ 140 | @spec determine_primitive_type_encoder(PrimitiveType.value_type()) :: String.t() 141 | def determine_primitive_type_encoder(value_type) do 142 | case value_type do 143 | :string -> "Encode.string" 144 | :integer -> "Encode.int" 145 | :number -> "Encode.float" 146 | :boolean -> "Encode.bool" 147 | :null -> "Encode.null" 148 | end 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /lib/printer/utils/elm_fuzzers.ex: -------------------------------------------------------------------------------- 1 | defmodule JS2E.Printer.Utils.ElmFuzzers do 2 | @moduledoc """ 3 | Module containing common utility functions for outputting Elm fuzzers and 4 | tests. 5 | """ 6 | 7 | require Logger 8 | alias JS2E.Printer 9 | alias JsonSchema.{Resolver, Types} 10 | alias Printer.{PrinterError, Utils} 11 | 12 | alias Types.{ 13 | AllOfType, 14 | ArrayType, 15 | ObjectType, 16 | PrimitiveType, 17 | SchemaDefinition 18 | } 19 | 20 | alias Utils.{CommonOperations, Naming} 21 | 22 | @type fuzzer_definition :: 23 | {:product, product_fuzzer} 24 | | {:tuple, tuple_fuzzer} 25 | | {:sum, sum_fuzzer} 26 | | {:list, list_fuzzer} 27 | 28 | @type product_fuzzer :: %{ 29 | name: String.t(), 30 | type: String.t(), 31 | argument_name: String.t(), 32 | decoder_name: String.t(), 33 | encoder_name: String.t(), 34 | field_fuzzers: [field_fuzzer] 35 | } 36 | 37 | @type tuple_fuzzer :: %{ 38 | name: String.t(), 39 | type: String.t(), 40 | argument_name: String.t(), 41 | decoder_name: String.t(), 42 | encoder_name: String.t(), 43 | field_fuzzers: [field_fuzzer] 44 | } 45 | 46 | @type field_fuzzer :: %{field_name: String.t(), fuzzer_name: String.t()} 47 | 48 | @type sum_fuzzer :: %{ 49 | name: String.t(), 50 | type: String.t(), 51 | argument_name: String.t(), 52 | decoder_name: String.t(), 53 | encoder_name: String.t(), 54 | clause_fuzzers: [String.t()] 55 | } 56 | 57 | @type list_fuzzer :: %{ 58 | name: String.t(), 59 | array_name: String.t(), 60 | items_type: String.t(), 61 | items_fuzzer: String.t(), 62 | argument_name: String.t(), 63 | decoder_name: String.t(), 64 | encoder_name: String.t() 65 | } 66 | 67 | @spec create_fuzzer_names( 68 | String.t(), 69 | Types.typeDefinition(), 70 | SchemaDefinition.t(), 71 | SchemaDefinition.t(), 72 | Types.schemaDictionary(), 73 | String.t() 74 | ) :: {:ok, [field_fuzzer()]} | {:error, PrinterError.t()} 75 | def create_fuzzer_names( 76 | property_name, 77 | resolved_type, 78 | resolved_schema, 79 | context_schema, 80 | schema_dict, 81 | module_name 82 | ) do 83 | if resolved_type.name == :anonymous do 84 | {resolved_pairs, errors} = 85 | case resolved_type do 86 | %AllOfType{} -> 87 | resolved_type.types 88 | |> Enum.map(fn {_name, path} -> 89 | Resolver.resolve_type( 90 | path, 91 | resolved_type.path, 92 | context_schema, 93 | schema_dict 94 | ) 95 | end) 96 | |> CommonOperations.split_ok_and_errors() 97 | 98 | %ArrayType{} -> 99 | {[], []} 100 | 101 | %ObjectType{} -> 102 | resolved_type.properties 103 | |> Enum.map(fn {_name, path} -> 104 | Resolver.resolve_type( 105 | path, 106 | resolved_type.path, 107 | context_schema, 108 | schema_dict 109 | ) 110 | end) 111 | |> CommonOperations.split_ok_and_errors() 112 | 113 | _ -> 114 | # TODO: Other cases? 115 | {[], []} 116 | end 117 | 118 | if errors != [] do 119 | {:error, errors} 120 | else 121 | fuzzer_names = 122 | resolved_pairs 123 | |> Enum.map(fn {resolved_type, schema} -> 124 | do_create_fuzzer_name( 125 | resolved_type.name, 126 | resolved_type, 127 | schema, 128 | context_schema, 129 | module_name 130 | ) 131 | end) 132 | 133 | {:ok, fuzzer_names} 134 | end 135 | else 136 | fuzzer_name = 137 | do_create_fuzzer_name( 138 | property_name, 139 | resolved_type, 140 | resolved_schema, 141 | context_schema, 142 | module_name 143 | ) 144 | 145 | {:ok, [fuzzer_name]} 146 | end 147 | end 148 | 149 | @spec do_create_fuzzer_name( 150 | String.t(), 151 | Types.typeDefinition(), 152 | SchemaDefinition.t(), 153 | SchemaDefinition.t(), 154 | String.t() 155 | ) :: field_fuzzer() 156 | defp do_create_fuzzer_name( 157 | property_name, 158 | resolved_type, 159 | resolved_schema, 160 | context_schema, 161 | module_name 162 | ) do 163 | fuzzer_name = 164 | case resolved_type do 165 | %PrimitiveType{} -> 166 | determine_primitive_fuzzer_name(resolved_type.type) 167 | 168 | _ -> 169 | downcased_type_name = Naming.normalize_identifier(resolved_type.name, :downcase) 170 | "#{downcased_type_name}Fuzzer" 171 | end 172 | 173 | if resolved_schema.id != context_schema.id do 174 | %{ 175 | field_name: property_name, 176 | fuzzer_name: Naming.qualify_name(resolved_schema, fuzzer_name, module_name) 177 | } 178 | else 179 | %{field_name: property_name, fuzzer_name: fuzzer_name} 180 | end 181 | end 182 | 183 | @doc ~S""" 184 | Converts a primitive value type into the corresponding Elm fuzzer. 185 | 186 | ## Examples 187 | 188 | iex> determine_primitive_fuzzer_name(:string) 189 | "Fuzz.string" 190 | 191 | iex> determine_primitive_fuzzer_name(:integer) 192 | "Fuzz.int" 193 | 194 | iex> determine_primitive_fuzzer_name(:number) 195 | "Fuzz.niceFloat" 196 | 197 | iex> determine_primitive_fuzzer_name(:boolean) 198 | "Fuzz.bool" 199 | 200 | """ 201 | @spec determine_primitive_fuzzer_name(PrimitiveType.value_type()) :: String.t() 202 | def determine_primitive_fuzzer_name(value_type) do 203 | case value_type do 204 | :string -> "Fuzz.string" 205 | :number -> "Fuzz.niceFloat" 206 | :integer -> "Fuzz.int" 207 | :boolean -> "Fuzz.bool" 208 | :null -> "Fuzzer.unit" 209 | end 210 | end 211 | end 212 | -------------------------------------------------------------------------------- /lib/printer/utils/elm_types.ex: -------------------------------------------------------------------------------- 1 | defmodule JS2E.Printer.Utils.ElmTypes do 2 | @moduledoc """ 3 | Module containing common utility functions for outputting Elm `type` 4 | and `type alias` definitions. 5 | """ 6 | 7 | require Logger 8 | alias JS2E.Printer 9 | alias JsonSchema.{Resolver, Types} 10 | alias Printer.{PrinterError, Utils} 11 | alias Types.{ArrayType, ObjectType, PrimitiveType, SchemaDefinition} 12 | alias Utils.{CommonOperations, Naming} 13 | 14 | @type type_definition :: {:product, product_type()} | {:sum, sum_type()} 15 | 16 | @type product_type :: %{ 17 | name: String.t(), 18 | fields: {:named, [named_field()]} | {:anonymous, [anonymous_field()]} 19 | } 20 | @type anonymous_field :: %{type: String.t()} 21 | @type named_field :: %{name: String.t(), type: String.t()} 22 | 23 | @type sum_type :: %{ 24 | name: String.t(), 25 | clauses: {:named, [named_clause()]} | {:anonymous, [String.t()]} 26 | } 27 | @type named_clause :: %{name: String.t(), type: String.t()} 28 | 29 | @spec create_fields( 30 | String.t() | :anonymous, 31 | Types.typeDefinition(), 32 | SchemaDefinition.t(), 33 | URI.t(), 34 | SchemaDefinition.t(), 35 | Types.schemaDictionary(), 36 | String.t() 37 | ) :: 38 | {:ok, [named_field()]} 39 | | {:error, PrinterError.t()} 40 | def create_fields( 41 | property_name, 42 | resolved_type, 43 | resolved_schema, 44 | parent, 45 | context_schema, 46 | schema_dict, 47 | module_name 48 | ) do 49 | if resolved_type.name == :anonymous do 50 | do_create_fields( 51 | resolved_type, 52 | resolved_schema, 53 | parent, 54 | context_schema, 55 | schema_dict 56 | ) 57 | else 58 | do_create_field( 59 | property_name, 60 | resolved_type, 61 | resolved_schema, 62 | parent, 63 | context_schema, 64 | schema_dict, 65 | module_name 66 | ) 67 | end 68 | end 69 | 70 | @spec do_create_fields( 71 | Types.typeDefinition(), 72 | SchemaDefinition.t(), 73 | URI.t(), 74 | SchemaDefinition.t(), 75 | Types.schemaDictionary() 76 | ) :: 77 | {:ok, [named_field()]} | {:error, PrinterError.t()} 78 | def do_create_fields( 79 | resolved_type, 80 | _resolved_schema, 81 | _parent, 82 | context_schema, 83 | schema_dict 84 | ) do 85 | case resolved_type do 86 | %ObjectType{} -> 87 | {type_names, errors} = 88 | resolved_type.properties 89 | |> Enum.map(fn {name, path} -> 90 | case Resolver.resolve_type( 91 | path, 92 | resolved_type.path, 93 | context_schema, 94 | schema_dict 95 | ) do 96 | {:ok, {property_type, _property_schema}} -> 97 | case property_type do 98 | %PrimitiveType{} -> 99 | primitive_type = determine_primitive_type_name(property_type.type) 100 | {:ok, %{name: name, type: primitive_type}} 101 | 102 | _ -> 103 | {:ok, 104 | %{ 105 | name: name, 106 | type: Naming.upcase_first(property_type.name) 107 | }} 108 | end 109 | 110 | {:error, error} -> 111 | {:error, error} 112 | end 113 | end) 114 | |> CommonOperations.split_ok_and_errors() 115 | 116 | if errors != [] do 117 | {:error, errors} 118 | else 119 | {:ok, type_names} 120 | end 121 | 122 | _ -> 123 | # TODO: Other cases? 124 | {:ok, []} 125 | end 126 | end 127 | 128 | @spec do_create_field( 129 | String.t() | :anonymous, 130 | Types.typeDefinition(), 131 | SchemaDefinition.t(), 132 | URI.t(), 133 | SchemaDefinition.t(), 134 | Types.schemaDictionary(), 135 | String.t() 136 | ) :: {:ok, [named_field()]} | {:error, PrinterError.t()} 137 | def do_create_field( 138 | property_name, 139 | resolved_type, 140 | resolved_schema, 141 | parent, 142 | context_schema, 143 | schema_dict, 144 | module_name 145 | ) do 146 | case resolved_type do 147 | %ArrayType{} -> 148 | case Resolver.resolve_type( 149 | resolved_type.items, 150 | parent, 151 | context_schema, 152 | schema_dict 153 | ) do 154 | {:ok, {items_type, _items_schema}} -> 155 | case items_type do 156 | %PrimitiveType{} -> 157 | primitive_type = determine_primitive_type_name(items_type.type) 158 | 159 | {:ok, 160 | [ 161 | %{ 162 | name: property_name, 163 | type: "List #{primitive_type}" 164 | } 165 | ]} 166 | 167 | _ -> 168 | type_name = "List #{Naming.upcase_first(items_type.name)}" 169 | 170 | field = 171 | check_qualified_name( 172 | property_name, 173 | type_name, 174 | resolved_schema, 175 | context_schema, 176 | module_name 177 | ) 178 | 179 | {:ok, [field]} 180 | end 181 | 182 | {:error, error} -> 183 | {:error, error} 184 | end 185 | 186 | %PrimitiveType{} -> 187 | primitive_type = determine_primitive_type_name(resolved_type.type) 188 | {:ok, [%{name: property_name, type: primitive_type}]} 189 | 190 | _ -> 191 | type_name = Naming.upcase_first(resolved_type.name) 192 | 193 | field = 194 | check_qualified_name( 195 | property_name, 196 | type_name, 197 | resolved_schema, 198 | context_schema, 199 | module_name 200 | ) 201 | 202 | {:ok, [field]} 203 | end 204 | end 205 | 206 | @spec check_qualified_name( 207 | String.t(), 208 | String.t(), 209 | SchemaDefinition.t(), 210 | SchemaDefinition.t(), 211 | String.t() 212 | ) :: named_field() 213 | defp check_qualified_name( 214 | property_name, 215 | type_name, 216 | resolved_schema, 217 | context_schema, 218 | module_name 219 | ) do 220 | if resolved_schema.id != context_schema.id do 221 | field_name = 222 | if property_name != :anonymous do 223 | property_name |> Naming.downcase_first() 224 | else 225 | type_name |> Naming.downcase_first() 226 | end 227 | 228 | qualified_type_name = Naming.qualify_name(resolved_schema, type_name, module_name) 229 | field_type_name = qualified_type_name |> Naming.upcase_first() 230 | %{name: field_name, type: field_type_name} 231 | else 232 | field_name = 233 | if property_name != :anonymous do 234 | property_name |> Naming.downcase_first() 235 | else 236 | type_name |> Naming.downcase_first() 237 | end 238 | 239 | field_type_name = type_name |> Naming.upcase_first() 240 | %{name: field_name, type: field_type_name} 241 | end 242 | end 243 | 244 | @doc ~S""" 245 | Converts a primitive value type into the corresponding Elm type. 246 | 247 | ## Examples 248 | 249 | iex> determine_primitive_type_name(:string) 250 | "String" 251 | 252 | iex> determine_primitive_type_name(:integer) 253 | "Int" 254 | 255 | iex> determine_primitive_type_name(:number) 256 | "Float" 257 | 258 | iex> determine_primitive_type_name(:boolean) 259 | "Bool" 260 | """ 261 | @spec determine_primitive_type_name(PrimitiveType.value_type()) :: String.t() 262 | def determine_primitive_type_name(value_type) do 263 | case value_type do 264 | :string -> "String" 265 | :integer -> "Int" 266 | :number -> "Float" 267 | :boolean -> "Bool" 268 | :null -> "()" 269 | end 270 | end 271 | end 272 | -------------------------------------------------------------------------------- /lib/printer/utils/indentation.ex: -------------------------------------------------------------------------------- 1 | defmodule JS2E.Printer.Utils.Indentation do 2 | @moduledoc ~S""" 3 | Module containing various utility functions for normalizing names of 4 | identifiers in the Elm output. 5 | """ 6 | 7 | @indent_size 4 8 | 9 | @doc ~S""" 10 | Returns a chunk of indentation. 11 | 12 | ## Examples 13 | 14 | iex> indent(2) 15 | " " 16 | 17 | """ 18 | @spec indent(pos_integer) :: String.t() 19 | def indent(tabs \\ 1) when is_integer(tabs) do 20 | String.pad_leading("", tabs * @indent_size) 21 | end 22 | 23 | @doc ~S""" 24 | Remove excessive newlines of a string. 25 | """ 26 | @spec trim_newlines(String.t()) :: String.t() 27 | def trim_newlines(str) do 28 | String.trim(str) <> "\n" 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/printer/utils/naming.ex: -------------------------------------------------------------------------------- 1 | defmodule JS2E.Printer.Utils.Naming do 2 | @moduledoc """ 3 | Module containing various utility functions for normalizing names of 4 | identifiers in the Elm output. 5 | """ 6 | 7 | alias JsonSchema.Types.SchemaDefinition 8 | 9 | @spec create_root_name(String.t(), SchemaDefinition.t()) :: String.t() 10 | def create_root_name(name, schema_def) do 11 | normalized_name = normalize_identifier(name, :upcase) 12 | 13 | if normalized_name == "Hash" do 14 | if schema_def.title != nil do 15 | upcase_first(schema_def.title) 16 | else 17 | "Root" 18 | end 19 | else 20 | normalized_name 21 | end 22 | end 23 | 24 | @spec qualify_name(SchemaDefinition.t(), String.t(), String.t()) :: String.t() 25 | def qualify_name(schema_def, type_name, _module_name) do 26 | schema_name = schema_def.title 27 | 28 | if String.length(schema_name) > 0 do 29 | "#{schema_name}.#{type_name}" 30 | else 31 | "#{type_name}" 32 | end 33 | end 34 | 35 | @type casing :: :upcase | :downcase | :none 36 | 37 | @doc ~S""" 38 | Normalizes a given identifier, i.e. translates numbers into plain 39 | text, e.g. '0' becomes 'zero', and translates symbols into plain text, 40 | e.g. '@' becomes 'at'. 41 | 42 | Also turns kebab-case, snake_case, and space case into camelCase. 43 | 44 | ## Examples 45 | 46 | iex> normalize_identifier("0") 47 | "zero" 48 | 49 | iex> normalize_identifier("shape") 50 | "shape" 51 | 52 | iex> normalize_identifier("myAngry!!Name") 53 | "myAngryBangBangName" 54 | 55 | iex> normalize_identifier("name@Domain") 56 | "nameAtDomain" 57 | 58 | iex> normalize_identifier("#Browns") 59 | "hashBrowns" 60 | 61 | iex> normalize_identifier("$Bill") 62 | "dollarBill" 63 | 64 | iex> normalize_identifier("identity") 65 | "identity" 66 | 67 | iex> normalize_identifier("i want to be camel cased") 68 | "iWantToBeCamelCased" 69 | 70 | iex> normalize_identifier("i-want-to-be-camel-cased") 71 | "iWantToBeCamelCased" 72 | 73 | iex> normalize_identifier("DontEverChange") 74 | "dontEverChange" 75 | 76 | iex> normalize_identifier("i_want_to_be_camel_cased") 77 | "iWantToBeCamelCased" 78 | 79 | """ 80 | @spec normalize_identifier(String.t(), casing) :: String.t() 81 | def normalize_identifier(identifier, casing \\ :none) do 82 | normalized_identifier = 83 | identifier 84 | |> kebab_to_camel_case 85 | |> snake_to_camel_case 86 | |> space_to_camel_case 87 | |> normalize_name 88 | |> normalize_symbols 89 | |> downcase_first 90 | 91 | case casing do 92 | :none -> 93 | if determine_case(identifier) == :upcase do 94 | upcase_first(normalized_identifier) 95 | else 96 | # We prefer to downcase 97 | downcase_first(normalized_identifier) 98 | end 99 | 100 | :upcase -> 101 | upcase_first(normalized_identifier) 102 | 103 | :downcase -> 104 | downcase_first(normalized_identifier) 105 | end 106 | end 107 | 108 | # Determines the casing of a string, e.g. 109 | # - the string `"Abc"` returns `:upcase`, 110 | # - the string `"abc"` returns `:downcase`, and 111 | # - the string `"$bc"` returns `:none` 112 | defp determine_case(s) do 113 | first = s 114 | 115 | cond do 116 | first |> String.upcase() == first and first |> String.downcase() != first -> 117 | :upcase 118 | 119 | first |> String.upcase() != first and first |> String.downcase() == first -> 120 | :downcase 121 | 122 | true -> 123 | :none 124 | end 125 | end 126 | 127 | # Prettifies anonymous schema names like `0` and `1` into - slightly - 128 | # better names like `zero` and `one` 129 | @spec normalize_name(String.t()) :: String.t() 130 | defp normalize_name("0"), do: "zero" 131 | defp normalize_name("1"), do: "one" 132 | defp normalize_name("2"), do: "two" 133 | defp normalize_name("3"), do: "three" 134 | defp normalize_name("4"), do: "four" 135 | defp normalize_name("5"), do: "five" 136 | defp normalize_name("6"), do: "six" 137 | defp normalize_name("7"), do: "seven" 138 | defp normalize_name("8"), do: "eight" 139 | defp normalize_name("9"), do: "nine" 140 | defp normalize_name("10"), do: "ten" 141 | defp normalize_name(name), do: downcase_first(name) 142 | 143 | # Filters out or translates all symbols that the Elm compiler does not allow 144 | # in an identifier: 145 | 146 | # ?!@#$%^&*()[]{}\/<>|`'",.+~=:; 147 | 148 | # into something more Elm parser friendly. Note that hyphens (-) and 149 | # underscores (_) should be converted to camelCase using the appropriate 150 | # helper functions. 151 | @spec normalize_symbols(String.t()) :: String.t() 152 | defp normalize_symbols(str) do 153 | str 154 | |> String.replace("?", "Huh") 155 | |> String.replace("!", "Bang") 156 | |> String.replace("@", "At") 157 | |> String.replace("#", "Hash") 158 | |> String.replace("$", "Dollar") 159 | |> String.replace("%", "Percent") 160 | |> String.replace("^", "Hat") 161 | |> String.replace("&", "And") 162 | |> String.replace("*", "Times") 163 | |> String.replace("(", "LParen") 164 | |> String.replace(")", "RParen") 165 | |> String.replace("[", "LBracket") 166 | |> String.replace("]", "RBracket") 167 | |> String.replace("{", "LBrace") 168 | |> String.replace("}", "RBrace") 169 | |> String.replace("<", "Lt") 170 | |> String.replace(">", "Gt") 171 | |> String.replace("\\", "Backslash") 172 | |> String.replace("/", "Slash") 173 | |> String.replace("|", "Pipe") 174 | |> String.replace("`", "Tick") 175 | |> String.replace("'", "Quote") 176 | |> String.replace("\"", "DoubleQuote") 177 | |> String.replace(".", "Dot") 178 | |> String.replace(",", "Comma") 179 | |> String.replace("-", "Minus") 180 | |> String.replace("+", "Plus") 181 | |> String.replace("~", "Tilde") 182 | |> String.replace("=", "Equal") 183 | |> String.replace(":", "Colon") 184 | |> String.replace(";", "Semicolon") 185 | end 186 | 187 | # Turns a kebab-cased identifier into a camelCased one 188 | @spec kebab_to_camel_case(String.t()) :: String.t() 189 | defp kebab_to_camel_case(str) do 190 | str 191 | |> String.split("-") 192 | |> Enum.map_join(fn word -> upcase_first(word) end) 193 | end 194 | 195 | # Turns a snake_cased identifier into a camelCased one 196 | @spec snake_to_camel_case(String.t()) :: String.t() 197 | defp snake_to_camel_case(str) do 198 | str 199 | |> String.split("_") 200 | |> Enum.map_join(fn word -> upcase_first(word) end) 201 | end 202 | 203 | # Turns a space cased identifier into a camelCased one 204 | @spec space_to_camel_case(String.t()) :: String.t() 205 | defp space_to_camel_case(str) do 206 | str 207 | |> String.split(" ") 208 | |> Enum.map_join(fn word -> upcase_first(word) end) 209 | end 210 | 211 | @doc ~S""" 212 | Upcases the first letter of a string. 213 | 214 | ## Examples 215 | 216 | iex> upcase_first("foobar") 217 | "Foobar" 218 | 219 | """ 220 | @spec upcase_first(String.t()) :: String.t() 221 | def upcase_first(string) when is_binary(string) do 222 | if String.length(string) > 0 do 223 | String.upcase(String.at(string, 0)) <> String.slice(string, 1..-1) 224 | else 225 | "" 226 | end 227 | end 228 | 229 | @doc ~S""" 230 | Downcases the first letter of a string. 231 | 232 | ## Examples 233 | 234 | iex> downcase_first("Foobar") 235 | "foobar" 236 | 237 | """ 238 | @spec downcase_first(String.t()) :: String.t() 239 | def downcase_first(string) when is_binary(string) do 240 | if String.length(string) > 0 do 241 | String.downcase(String.at(string, 0)) <> String.slice(string, 1..-1) 242 | else 243 | "" 244 | end 245 | end 246 | end 247 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule JS2E.MixProject do 2 | use Mix.Project 3 | 4 | @version "2.9.1" 5 | @elixir_version "~> 1.14" 6 | 7 | def project do 8 | [ 9 | app: :js2e, 10 | version: @version, 11 | elixir: @elixir_version, 12 | aliases: aliases(), 13 | deps: deps(), 14 | description: description(), 15 | dialyzer: dialyzer(), 16 | docs: docs(), 17 | escript: escript(), 18 | preferred_cli_env: preferred_cli_env(), 19 | test_coverage: test_coverage(), 20 | build_embedded: Mix.env() == :prod, 21 | start_permanent: Mix.env() == :prod 22 | ] 23 | end 24 | 25 | def application do 26 | [extra_applications: [:logger, :eex]] 27 | end 28 | 29 | defp aliases do 30 | [ 31 | build: ["deps.get", "compile", "escript.build"], 32 | check: ["credo --strict --ignore=RedundantBlankLines"] 33 | ] 34 | end 35 | 36 | defp deps do 37 | [ 38 | {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, 39 | {:dialyxir, "~> 1.2", only: [:dev], runtime: false}, 40 | {:ex_doc, "~> 0.29", only: :dev, runtime: false}, 41 | {:excoveralls, "~> 0.16", only: :test, runtime: false}, 42 | {:gradient, github: "esl/gradient", only: [:dev], runtime: false}, 43 | # {:json_schema, path: "../json_schema/"}, 44 | {:json_schema, "~> 0.5"}, 45 | {:typed_struct, "~> 0.3"} 46 | ] 47 | end 48 | 49 | defp description do 50 | """ 51 | Generates Elm types, JSON decoders, JSON encoders and fuzz tests from JSON 52 | schema specifications. 53 | """ 54 | end 55 | 56 | defp dialyzer do 57 | [plt_add_deps: :apps_direct] 58 | end 59 | 60 | defp docs do 61 | [ 62 | name: "JSON Schema to Elm", 63 | formatter_opts: [gfm: true], 64 | source_ref: @version, 65 | source_url: "https://github.com/dragonwasrobot/json-schema-to-elm", 66 | extras: [] 67 | ] 68 | end 69 | 70 | defp escript do 71 | [main_module: JS2E, name: "js2e"] 72 | end 73 | 74 | defp preferred_cli_env do 75 | [ 76 | coveralls: :test, 77 | "coveralls.detail": :test, 78 | "coveralls.post": :test, 79 | "coveralls.html": :test 80 | ] 81 | end 82 | 83 | defp test_coverage do 84 | [tool: ExCoveralls] 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, 3 | "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, 4 | "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, 5 | "dialyxir": {:hex, :dialyxir, "1.3.0", "fd1672f0922b7648ff9ce7b1b26fcf0ef56dda964a459892ad15f6b4410b5284", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "00b2a4bcd6aa8db9dcb0b38c1225b7277dca9bc370b6438715667071a304696f"}, 6 | "earmark_parser": {:hex, :earmark_parser, "1.4.31", "a93921cdc6b9b869f519213d5bc79d9e218ba768d7270d46fdcf1c01bacff9e2", [:mix], [], "hexpm", "317d367ee0335ef037a87e46c91a2269fef6306413f731e8ec11fc45a7efd059"}, 7 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 8 | "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"}, 9 | "excoveralls": {:hex, :excoveralls, "0.16.1", "0bd42ed05c7d2f4d180331a20113ec537be509da31fed5c8f7047ce59ee5a7c5", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "dae763468e2008cf7075a64cb1249c97cb4bc71e236c5c2b5e5cdf1cfa2bf138"}, 10 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 11 | "gradient": {:git, "https://github.com/esl/gradient.git", "a8ceef28279bbf5fe4a8a6fec17bfb03a5ce731d", []}, 12 | "gradualizer": {:git, "https://github.com/josefs/Gradualizer.git", "ba5481cf1c208a0c29d15e5ab241b5af58dde28d", [ref: "ba5481c"]}, 13 | "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, 14 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 15 | "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, 16 | "json_schema": {:hex, :json_schema, "0.5.0", "95419ef05b1f00af618aeb2cb3be69c618aebd6f5342c1d272e8e5661d24939c", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.3", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "476fbc5d07996acdbfd802137f7338fe01f8949d98cf58d9540e5b92b262bf19"}, 17 | "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, 18 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, 19 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 20 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 21 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 22 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, 23 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 24 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, 25 | "typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"}, 26 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 27 | } 28 | -------------------------------------------------------------------------------- /priv/templates/decoders/enum_decoder.elm.eex: -------------------------------------------------------------------------------- 1 | <%= enum_decoder.name %> : Decoder <%= enum_decoder.type %> 2 | <%= enum_decoder.name %> = 3 | Decode.<%= enum_decoder.argument_type %> |> Decode.andThen (<%= enum_decoder.parser_name %> >> Decode.fromResult) 4 | 5 | 6 | <%= enum_decoder.parser_name %> : <%= String.capitalize(enum_decoder.argument_type) %> -> Result String <%= enum_decoder.type %> 7 | <%= enum_decoder.parser_name %> <%= enum_decoder.argument_name %> = 8 | case <%= enum_decoder.argument_name %> of<%# 9 | %><%= for clause <- enum_decoder.clauses do %> 10 | <%= clause.raw_value %> -> 11 | Ok <%= clause.parsed_value %> 12 | <%# %><% end %> 13 | _ -> 14 | Err <| "Unknown <%= enum_decoder.argument_name %> type: " ++ <%= enum_decoder.argument_name %> 15 | -------------------------------------------------------------------------------- /priv/templates/decoders/list_decoder.elm.eex: -------------------------------------------------------------------------------- 1 | <%= list_decoder.name %> : Decoder (List <%= list_decoder.type %>) 2 | <%= list_decoder.name %> = 3 | Decode.list <%= list_decoder.item_decoder %> 4 | -------------------------------------------------------------------------------- /priv/templates/decoders/product_decoder.elm.eex: -------------------------------------------------------------------------------- 1 | <%= product_decoder.name %> : Decoder <%= product_decoder.type %> 2 | <%= product_decoder.name %> =<%# 3 | %><%= case product_decoder.clauses do %> 4 | <% {:anonymous, clauses} -> %> 5 | Decode.map<%= Enum.count(clauses) %> (\<%= Enum.map_join(clauses, " ", fn clause -> clause.property_name end) %> -> (<%= Enum.map_join(clauses, ", ", fn clause -> clause.property_name end) %>))<%# 6 | %><%= for {clause, idx} <- Enum.with_index(clauses) do %> 7 | (Decode.index <%= idx %> <%= clause.decoder_name %>)<%# 8 | %><% end %><%# 9 | %><% {:named, clauses} -> %> 10 | Decode.succeed <%= product_decoder.type %><%# 11 | %><%= for clause <- clauses do %> 12 | <%= if clause.option == :custom do %><%# 13 | %>|> custom <%= clause.decoder_name %><%# 14 | %><% else %><%# 15 | %>|> <%= clause.option %> "<%= clause.property_name %>" <%= clause.decoder_name %><%# 16 | %><% end %><%# 17 | %><% end %><%# 18 | %><% end %> 19 | -------------------------------------------------------------------------------- /priv/templates/decoders/sum_decoder.elm.eex: -------------------------------------------------------------------------------- 1 | <%= sum_decoder.name %> : Decoder <%= sum_decoder.type %> 2 | <%= sum_decoder.name %> =<%# 3 | %><%= case sum_decoder.clauses do %> 4 | <% {:anonymous, clauses} -> %> 5 | Decode.oneOf [ <%= Enum.map_join(clauses, "\n , ", fn clause -> clause.decoder_name end) %><%# 6 | %><%= if sum_decoder.optional do "\n , Decode.null Nothing" end %> 7 | ]<%# 8 | %><% {:named, clauses} -> %> 9 | Decode.oneOf [ <%= Enum.map_join(clauses, "\n , ", fn clause -> "#{clause.decoder_name} |> Decode.map #{clause.constructor_name}" end) %><%# 10 | %><%= if sum_decoder.optional do "\n , Decode.null Nothing" end %> 11 | ]<%# 12 | %><% end %> 13 | -------------------------------------------------------------------------------- /priv/templates/encoders/enum_encoder.elm.eex: -------------------------------------------------------------------------------- 1 | <%= enum_encoder.name %> : <%= enum_encoder.type %> -> Value 2 | <%= enum_encoder.name %> <%= enum_encoder.argument_name %> = 3 | <%= enum_encoder.argument_name %> |> <%= enum_encoder.argument_name %>To<%= enum_encoder.argument_js_type %> |> Encode.<%= String.downcase(enum_encoder.argument_js_type) %> 4 | 5 | 6 | <%= enum_encoder.argument_name %>To<%= enum_encoder.argument_js_type %> : <%= enum_encoder.type %> -> <%= enum_encoder.argument_js_type %> 7 | <%= enum_encoder.argument_name %>To<%= enum_encoder.argument_js_type %> <%= enum_encoder.argument_name %> = 8 | case <%= enum_encoder.argument_name %> of<%# 9 | %><%= for clause <- enum_encoder.clauses do %> 10 | <%= clause.elm_value %> -> 11 | <%= clause.json_value %> 12 | <%# %><% end %> 13 | -------------------------------------------------------------------------------- /priv/templates/encoders/list_encoder.elm.eex: -------------------------------------------------------------------------------- 1 | <%= list_encoder.name %> : List <%= list_encoder.type %> -> Value 2 | <%= list_encoder.name %> <%= list_encoder.argument_name %> = 3 | <%= list_encoder.argument_name %> 4 | |> Encode.list <%= list_encoder.items_encoder %> 5 | -------------------------------------------------------------------------------- /priv/templates/encoders/product_encoder.elm.eex: -------------------------------------------------------------------------------- 1 | <%= product_encoder.name %> : <%= product_encoder.type %> -> Value 2 | <%= product_encoder.name %> <%= product_encoder.argument_name %> = 3 | []<%# 4 | %><%= for property <- product_encoder.properties do %> 5 | <%= if property.required == true do %><%# 6 | %>|> Encode.required "<%= property.name %>" <%= product_encoder.argument_name %>.<%= property.location %> <%= property.encoder_name %><%# 7 | %><% else %><%# 8 | %>|> Encode.optional "<%= property.name %>" <%= product_encoder.argument_name %>.<%= property.location %> <%= property.encoder_name %><%# 9 | %><% end %><%# 10 | %><% end %> 11 | |> Encode.object 12 | -------------------------------------------------------------------------------- /priv/templates/encoders/sum_encoder.elm.eex: -------------------------------------------------------------------------------- 1 | <%= sum_encoder.name %> : <%= sum_encoder.type %> -> Value 2 | <%= sum_encoder.name %> <%= sum_encoder.argument_name %> = 3 | case <%= sum_encoder.argument_name %> of<%# 4 | %><%= for case <- sum_encoder.cases do %> 5 | <%= case.constructor %> -> 6 | <%= case.encoder %> 7 | <%# %><% end %> 8 | -------------------------------------------------------------------------------- /priv/templates/encoders/tuple_encoder.elm.eex: -------------------------------------------------------------------------------- 1 | <%= tuple_encoder.name %> : <%= tuple_encoder.type %> -> Value 2 | <%= tuple_encoder.name %> (<%= Enum.map_join(tuple_encoder.properties, ", ", fn p -> p.name end) %>) = 3 | []<%# 4 | %><%= for property <- tuple_encoder.properties do %> 5 | |> (::) <%= property.encoder_name %> <%= property.name %><%# 6 | %><% end %> 7 | |> Encode.list 8 | -------------------------------------------------------------------------------- /priv/templates/fuzzers/list_fuzzer.elm.eex: -------------------------------------------------------------------------------- 1 | <%= list_fuzzer.name %> : Fuzzer (List <%= list_fuzzer.items_type %>) 2 | <%= list_fuzzer.name %> = 3 | Fuzz.list <%= list_fuzzer.items_fuzzer %> 4 | 5 | 6 | encodeDecode<%= list_fuzzer.array_name %>Test : Test 7 | encodeDecode<%= list_fuzzer.array_name %>Test = 8 | fuzz <%= list_fuzzer.name %> "can encode and decode <%= list_fuzzer.array_name %>" <| 9 | \<%= list_fuzzer.argument_name %> -> 10 | <%= list_fuzzer.argument_name %> 11 | |> <%= list_fuzzer.encoder_name %> 12 | |> Decode.decodeValue <%= list_fuzzer.decoder_name %> 13 | |> Expect.equal (Ok <%= list_fuzzer.argument_name %>) 14 | -------------------------------------------------------------------------------- /priv/templates/fuzzers/product_fuzzer.elm.eex: -------------------------------------------------------------------------------- 1 | <%= product_fuzzer.name %> : Fuzzer <%= product_fuzzer.type %> 2 | <%= product_fuzzer.name %> = 3 | Fuzz.map<%= if length(product_fuzzer.fuzzers) < 2 do "" else length(product_fuzzer.fuzzers) end %> 4 | <%= product_fuzzer.type %> 5 | <%= Enum.map_join(product_fuzzer.fuzzers, "\n ", &(&1.fuzzer_name)) %> 6 | 7 | 8 | encodeDecode<%= product_fuzzer.type %>Test : Test 9 | encodeDecode<%= product_fuzzer.type %>Test = 10 | fuzz <%= product_fuzzer.name %> "can encode and decode <%= product_fuzzer.type %> object" <| 11 | \<%= product_fuzzer.argument_name %> -> 12 | <%= product_fuzzer.argument_name %> 13 | |> <%= product_fuzzer.encoder_name %> 14 | |> Decode.decodeValue <%= product_fuzzer.decoder_name %> 15 | |> Expect.equal (Ok <%= product_fuzzer.argument_name %>) 16 | -------------------------------------------------------------------------------- /priv/templates/fuzzers/sum_fuzzer.elm.eex: -------------------------------------------------------------------------------- 1 | <%= sum_fuzzer.name %> : Fuzzer <%= sum_fuzzer.type %> 2 | <%= sum_fuzzer.name %> = 3 | Fuzz.oneOf 4 | [ <%= Enum.join(sum_fuzzer.clause_fuzzers, "\n , ") %> 5 | ] 6 | 7 | 8 | encodeDecode<%= sum_fuzzer.type %>Test : Test 9 | encodeDecode<%= sum_fuzzer.type %>Test = 10 | fuzz <%= sum_fuzzer.name %> "can encode and decode <%= sum_fuzzer.type %>" <| 11 | \<%= sum_fuzzer.argument_name %> -> 12 | <%= sum_fuzzer.argument_name %> 13 | |> <%= sum_fuzzer.encoder_name %> 14 | |> Decode.decodeValue <%= sum_fuzzer.decoder_name %> 15 | |> Expect.equal (Ok <%= sum_fuzzer.argument_name %>) 16 | -------------------------------------------------------------------------------- /priv/templates/fuzzers/tuple_fuzzer.elm.eex: -------------------------------------------------------------------------------- 1 | <%= tuple_fuzzer.name %> : Fuzzer <%= tuple_fuzzer.type %> 2 | <%= tuple_fuzzer.name %> = 3 | Fuzz.tuple<%= if length(tuple_fuzzer.field_fuzzers) > 2 do length(tuple_fuzzer.field_fuzzers) end %> 4 | (<%= Enum.map_join(tuple_fuzzer.field_fuzzers, "\n , ", fn f -> f.fuzzer_name end) %> 5 | ) 6 | 7 | 8 | encodeDecode<%= tuple_fuzzer.type %>Test : Test 9 | encodeDecode<%= tuple_fuzzer.type %>Test = 10 | fuzz <%= tuple_fuzzer.name %> "can encode and decode <%= tuple_fuzzer.type %> tuple" <| 11 | \<%= tuple_fuzzer.argument_name %> -> 12 | <%= tuple_fuzzer.argument_name %> 13 | |> <%= tuple_fuzzer.encoder_name %> 14 | |> Decode.decodeValue <%= tuple_fuzzer.decoder_name %> 15 | |> Expect.equal (Ok <%= tuple_fuzzer.argument_name %>) 16 | -------------------------------------------------------------------------------- /priv/templates/preamble/preamble.elm.eex: -------------------------------------------------------------------------------- 1 | module <%= prefix %><%= title %> exposing (..) 2 | 3 | -- <%= description %> 4 | 5 | import Helper.Encode as Encode 6 | import Json.Decode as Decode exposing (Decoder) 7 | import Json.Decode.Extra as Decode 8 | import Json.Decode.Pipeline 9 | exposing 10 | ( custom 11 | , optional 12 | , required 13 | ) 14 | import Json.Encode as Encode exposing (Value) 15 | <%= for import_name <- imports do %><%# 16 | %>import <%= prefix %><%= import_name %> as <%= import_name %> 17 | <% end %> 18 | -------------------------------------------------------------------------------- /priv/templates/preamble/tests_preamble.elm.eex: -------------------------------------------------------------------------------- 1 | module <%= prefix %><%= title %>Tests exposing (..) 2 | 3 | -- Tests: <%= description %> 4 | 5 | import Expect exposing (Expectation) 6 | import Fuzz exposing (Fuzzer) 7 | import Test exposing (..) 8 | import Json.Decode as Decode 9 | import <%= prefix %><%= title %> exposing (..) 10 | <%= for import_name <- imports do %><%# 11 | %>import <%= prefix %><%= import_name %>Tests as <%= import_name %> 12 | <% end %> 13 | -------------------------------------------------------------------------------- /priv/templates/project/elm.json.eex: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "NoRedInk/elm-json-decode-pipeline": "1.0.1", 10 | "elm/browser": "1.0.2", 11 | "elm/core": "1.0.5", 12 | "elm/html": "1.0.0", 13 | "elm-community/json-extra": "4.3.0" 14 | }, 15 | "indirect": { 16 | "elm/json": "1.1.3", 17 | "elm/parser": "1.1.0", 18 | "elm/time": "1.0.0", 19 | "elm/url": "1.0.0", 20 | "elm/virtual-dom": "1.0.3", 21 | "rtfeldman/elm-iso8601-date-strings": "1.1.4" 22 | } 23 | }, 24 | "test-dependencies": { 25 | "direct": { 26 | "elm-explorations/test": "2.1.1" 27 | }, 28 | "indirect": { 29 | "elm/bytes": "1.0.8", 30 | "elm/random": "1.0.0" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /priv/templates/project/package.json.eex: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js2e_output", 3 | "version": "1.0.0", 4 | "description": "Autogenerated Elm types, decoders, and encoders", 5 | "dependencies": { 6 | "elm-test": "^0.19.1-revision12" 7 | }, 8 | "scripts": { 9 | "test": "./node_modules/elm-test/bin/elm-test --fuzz 10" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /priv/templates/project/tool_versions.eex: -------------------------------------------------------------------------------- 1 | elm 0.19.1 2 | node 16.17.0 3 | -------------------------------------------------------------------------------- /priv/templates/types/product_type.elm.eex: -------------------------------------------------------------------------------- 1 | type alias <%= product_type.name %> =<%# 2 | %><%= case product_type.fields do %> 3 | <% {:anonymous, fields} -> %> 4 | ( <%= Enum.map_join(fields, "\n , ", fn field -> field.type end) %> 5 | )<%# 6 | %><% {:named, fields} -> %> 7 | { <%= Enum.map_join(fields, "\n , ", fn field -> "#{field.name} : #{field.type}" end) %> 8 | }<%# 9 | %><% end %> 10 | -------------------------------------------------------------------------------- /priv/templates/types/sum_type.elm.eex: -------------------------------------------------------------------------------- 1 | type <%= sum_type.name %><%# 2 | %><%= case sum_type.clauses do %> 3 | <% {:anonymous, clauses} -> %> 4 | = <%= Enum.join(clauses, "\n | ")%><%# 5 | %><% {:named, clauses} -> %> 6 | = <%= Enum.map_join(clauses, "\n | ", fn clause -> "#{clause.name} #{clause.type}" end) %><%# 7 | %><% end %> 8 | -------------------------------------------------------------------------------- /priv/templates/utils/encode.elm.eex: -------------------------------------------------------------------------------- 1 | module Helper.Encode exposing (optional, required) 2 | 3 | -- Helper functions for encoding JSON objects. 4 | 5 | import Json.Encode as Encode exposing (Value) 6 | 7 | 8 | required : 9 | String 10 | -> a 11 | -> (a -> Value) 12 | -> List ( String, Value ) 13 | -> List ( String, Value ) 14 | required key value encode properties = 15 | properties ++ [ ( key, encode value ) ] 16 | 17 | 18 | optional : 19 | String 20 | -> Maybe a 21 | -> (a -> Value) 22 | -> List ( String, Value ) 23 | -> List ( String, Value ) 24 | optional key maybe encode properties = 25 | case maybe of 26 | Just value -> 27 | properties ++ [ ( key, encode value ) ] 28 | 29 | Nothing -> 30 | properties 31 | -------------------------------------------------------------------------------- /test/printer/all_of_printer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule JS2ETest.Printer.AllOfPrinter do 2 | use ExUnit.Case 3 | require Logger 4 | alias JS2E.Printer.AllOfPrinter 5 | 6 | alias JsonSchema.Types.{ 7 | AllOfType, 8 | EnumType, 9 | ObjectType, 10 | PrimitiveType, 11 | SchemaDefinition, 12 | TypeReference 13 | } 14 | 15 | test "print 'all of' type value" do 16 | result = 17 | all_of_type() 18 | |> AllOfPrinter.print_type(schema_def(), %{}, module_name()) 19 | 20 | all_of_type_program = result.printed_schema 21 | 22 | expected_all_of_type_program = """ 23 | type alias FancyCircle = 24 | { circle : Circle 25 | , color : Color 26 | , description : Maybe String 27 | } 28 | """ 29 | 30 | assert all_of_type_program == expected_all_of_type_program 31 | end 32 | 33 | test "print 'all of' decoder" do 34 | result = 35 | all_of_type() 36 | |> AllOfPrinter.print_decoder(schema_def(), %{}, module_name()) 37 | 38 | expected_all_of_decoder_program = """ 39 | fancyCircleDecoder : Decoder FancyCircle 40 | fancyCircleDecoder = 41 | Decode.succeed FancyCircle 42 | |> custom circleDecoder 43 | |> required "color" colorDecoder 44 | |> optional "description" Decode.string 45 | """ 46 | 47 | all_of_decoder_program = result.printed_schema 48 | 49 | assert all_of_decoder_program == expected_all_of_decoder_program 50 | end 51 | 52 | test "print 'all of' encoder" do 53 | result = 54 | all_of_type() 55 | |> AllOfPrinter.print_encoder(schema_def(), %{}, module_name()) 56 | 57 | expected_all_of_encoder_program = """ 58 | encodeFancyCircle : FancyCircle -> Value 59 | encodeFancyCircle fancyCircle = 60 | [] 61 | |> Encode.required "color" fancyCircle.color encodeColor 62 | |> Encode.optional "description" fancyCircle.description Encode.string 63 | |> Encode.required "radius" fancyCircle.circle.radius Encode.float 64 | |> Encode.object 65 | """ 66 | 67 | all_of_encoder_program = result.printed_schema 68 | 69 | assert all_of_encoder_program == expected_all_of_encoder_program 70 | end 71 | 72 | test "print 'all of' fuzzer" do 73 | result = 74 | all_of_type() 75 | |> AllOfPrinter.print_fuzzer(schema_def(), %{}, module_name()) 76 | 77 | expected_all_of_fuzzer_program = """ 78 | fancyCircleFuzzer : Fuzzer FancyCircle 79 | fancyCircleFuzzer = 80 | Fuzz.map3 81 | FancyCircle 82 | circleFuzzer 83 | colorFuzzer 84 | Fuzz.string 85 | 86 | 87 | encodeDecodeFancyCircleTest : Test 88 | encodeDecodeFancyCircleTest = 89 | fuzz fancyCircleFuzzer "can encode and decode FancyCircle object" <| 90 | \\fancyCircle -> 91 | fancyCircle 92 | |> encodeFancyCircle 93 | |> Decode.decodeValue fancyCircleDecoder 94 | |> Expect.equal (Ok fancyCircle) 95 | """ 96 | 97 | all_of_fuzzer_program = result.printed_schema 98 | 99 | assert all_of_fuzzer_program == expected_all_of_fuzzer_program 100 | end 101 | 102 | defp path, do: "#/definitions/fancyCircle" 103 | def module_name, do: "Data" 104 | 105 | def all_of_type do 106 | %AllOfType{ 107 | name: "fancyCircle", 108 | path: URI.parse(path()), 109 | types: [ 110 | URI.parse(Path.join(path(), "allOf/0")), 111 | URI.parse(Path.join(path(), "allOf/1")) 112 | ] 113 | } 114 | end 115 | 116 | def schema_def do 117 | %SchemaDefinition{ 118 | description: "'allOf' example schema", 119 | id: URI.parse("http://example.com/all_of_example.json"), 120 | file_path: "all_of_example.json", 121 | title: "AllOfExample", 122 | types: type_dict() 123 | } 124 | end 125 | 126 | def type_dict do 127 | %{ 128 | "#/definitions/fancyCircle/allOf/0" => %ObjectType{ 129 | name: :anonymous, 130 | path: URI.parse(Path.join(path(), "allOf/0")), 131 | required: ["color"], 132 | properties: %{ 133 | "color" => URI.parse(Path.join(path(), "allOf/0/properties/color")), 134 | "description" => URI.parse(Path.join(path(), "allOf/0/properties/description")) 135 | }, 136 | pattern_properties: %{} 137 | }, 138 | "#/definitions/fancyCircle/allOf/0/properties/color" => %TypeReference{ 139 | name: "color", 140 | path: URI.parse("#/definitions/color") 141 | }, 142 | "#/definitions/color" => %EnumType{ 143 | name: "color", 144 | path: URI.parse("#/definitions/color"), 145 | type: "string", 146 | values: ["red", "yellow", "green"] 147 | }, 148 | "#/definitions/fancyCircle/allOf/0/properties/description" => %PrimitiveType{ 149 | name: "description", 150 | path: URI.parse(Path.join(path(), "allOf/0/properties/description")), 151 | type: :string 152 | }, 153 | "#/definitions/fancyCircle/allOf/1" => %TypeReference{ 154 | name: :anonymous, 155 | path: URI.parse("#/definitions/circle") 156 | }, 157 | "#/definitions/circle" => %ObjectType{ 158 | name: "circle", 159 | path: URI.parse("#/definitions/circle"), 160 | required: ["radius"], 161 | properties: %{ 162 | "radius" => URI.parse("#/definitions/circle/properties/radius") 163 | }, 164 | pattern_properties: %{} 165 | }, 166 | "#/definitions/circle/properties/radius" => %PrimitiveType{ 167 | name: "radius", 168 | path: URI.parse("#/definitions/circle/properties/radius"), 169 | type: :number 170 | } 171 | } 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /test/printer/any_of_printer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule JS2ETest.Printer.AnyOfPrinter do 2 | use ExUnit.Case 3 | require Logger 4 | alias JS2E.Printer.AnyOfPrinter 5 | 6 | alias JsonSchema.Types.{ 7 | AnyOfType, 8 | EnumType, 9 | ObjectType, 10 | PrimitiveType, 11 | SchemaDefinition, 12 | TypeReference 13 | } 14 | 15 | test "print 'any of' type value" do 16 | result = 17 | any_of_type() 18 | |> AnyOfPrinter.print_type(schema_def(), %{}, module_name()) 19 | 20 | any_of_type_program = result.printed_schema 21 | 22 | expected_any_of_type_program = """ 23 | type alias FancyCircle = 24 | { circle : Maybe Circle 25 | , color : Maybe Color 26 | , description : Maybe String 27 | } 28 | """ 29 | 30 | assert any_of_type_program == expected_any_of_type_program 31 | end 32 | 33 | test "print 'any of' decoder" do 34 | result = 35 | any_of_type() 36 | |> AnyOfPrinter.print_decoder(schema_def(), %{}, module_name()) 37 | 38 | expected_any_of_decoder_program = """ 39 | fancyCircleDecoder : Decoder FancyCircle 40 | fancyCircleDecoder = 41 | Decode.succeed FancyCircle 42 | |> custom (Decode.nullable circleDecoder) 43 | |> optional "color" (Decode.nullable colorDecoder) Nothing 44 | |> optional "description" (Decode.nullable Decode.string) Nothing 45 | """ 46 | 47 | any_of_decoder_program = result.printed_schema 48 | 49 | assert any_of_decoder_program == expected_any_of_decoder_program 50 | end 51 | 52 | test "print 'any of' encoder" do 53 | result = 54 | any_of_type() 55 | |> AnyOfPrinter.print_encoder(schema_def(), %{}, module_name()) 56 | 57 | any_of_encoder_program = result.printed_schema 58 | 59 | expected_any_of_encoder_program = """ 60 | encodeFancyCircle : FancyCircle -> Value 61 | encodeFancyCircle fancyCircle = 62 | [] 63 | |> Encode.optional "color" fancyCircle.color encodeColor 64 | |> Encode.optional "description" fancyCircle.description Encode.string 65 | |> Encode.optional "radius" fancyCircle.circle.radius Encode.float 66 | |> Encode.object 67 | """ 68 | 69 | assert any_of_encoder_program == expected_any_of_encoder_program 70 | end 71 | 72 | test "print 'any of' fuzzer" do 73 | result = 74 | any_of_type() 75 | |> AnyOfPrinter.print_fuzzer(schema_def(), %{}, module_name()) 76 | 77 | expected_any_of_fuzzer_program = """ 78 | fancyCircleFuzzer : Fuzzer FancyCircle 79 | fancyCircleFuzzer = 80 | Fuzz.map3 81 | FancyCircle 82 | (Fuzz.maybe circleFuzzer) 83 | (Fuzz.maybe colorFuzzer) 84 | (Fuzz.maybe Fuzz.string) 85 | 86 | 87 | encodeDecodeFancyCircleTest : Test 88 | encodeDecodeFancyCircleTest = 89 | fuzz fancyCircleFuzzer "can encode and decode FancyCircle object" <| 90 | \\fancyCircle -> 91 | fancyCircle 92 | |> encodeFancyCircle 93 | |> Decode.decodeValue fancyCircleDecoder 94 | |> Expect.equal (Ok fancyCircle) 95 | """ 96 | 97 | any_of_fuzzer_program = result.printed_schema 98 | 99 | assert any_of_fuzzer_program == expected_any_of_fuzzer_program 100 | end 101 | 102 | def module_name, do: "Data" 103 | defp path, do: "#/definitions/fancyCircle" 104 | 105 | def any_of_type do 106 | %AnyOfType{ 107 | name: "fancyCircle", 108 | path: URI.parse(path()), 109 | types: [ 110 | URI.parse(Path.join(path(), "anyOf/0")), 111 | URI.parse(Path.join(path(), "anyOf/1")) 112 | ] 113 | } 114 | end 115 | 116 | def schema_def do 117 | %SchemaDefinition{ 118 | description: "'anyOf' example schema", 119 | id: URI.parse("http://example.com/any_of_example.json"), 120 | file_path: "any_of_example.json", 121 | title: "AnyOfExample", 122 | types: type_dict() 123 | } 124 | end 125 | 126 | def type_dict do 127 | %{ 128 | "#/definitions/fancyCircle/anyOf/0" => %ObjectType{ 129 | name: :anonymous, 130 | path: URI.parse(Path.join(path(), "anyOf/0")), 131 | required: ["color"], 132 | properties: %{ 133 | "color" => URI.parse(Path.join(path(), "anyOf/0/properties/color")), 134 | "description" => URI.parse(Path.join(path(), "anyOf/0/properties/description")) 135 | }, 136 | pattern_properties: %{} 137 | }, 138 | "#/definitions/fancyCircle/anyOf/0/properties/color" => %TypeReference{ 139 | name: "color", 140 | path: URI.parse("#/definitions/color") 141 | }, 142 | "#/definitions/color" => %EnumType{ 143 | name: "color", 144 | path: URI.parse("#/definitions/color"), 145 | type: "string", 146 | values: ["red", "yellow", "green"] 147 | }, 148 | "#/definitions/fancyCircle/anyOf/0/properties/description" => %PrimitiveType{ 149 | name: "description", 150 | path: URI.parse(Path.join(path(), "anyOf/0/properties/description")), 151 | type: :string 152 | }, 153 | "#/definitions/fancyCircle/anyOf/1" => %TypeReference{ 154 | name: "1", 155 | path: URI.parse("#/definitions/circle") 156 | }, 157 | "#/definitions/circle" => %ObjectType{ 158 | name: "circle", 159 | path: URI.parse("#/definitions/circle"), 160 | required: ["radius"], 161 | properties: %{ 162 | "radius" => URI.parse("#/definitions/circle/properties/radius") 163 | }, 164 | pattern_properties: %{} 165 | }, 166 | "#/definitions/circle/properties/radius" => %PrimitiveType{ 167 | name: "radius", 168 | path: URI.parse("#/definitions/circle/properties/radius"), 169 | type: :number 170 | } 171 | } 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /test/printer/array_printer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule JS2ETest.Printer.ArrayPrinter do 2 | use ExUnit.Case 3 | 4 | require Logger 5 | alias JS2E.Printer 6 | alias JsonSchema.Types 7 | alias Printer.ArrayPrinter 8 | alias Types.{ArrayType, EnumType, PrimitiveType, SchemaDefinition} 9 | 10 | # Array with primitive type 11 | 12 | test "print array primitive type" do 13 | result = 14 | array_type_primitive() 15 | |> ArrayPrinter.print_type(schema_def(), %{}, module_name()) 16 | 17 | expected_array_type_program = "" 18 | array_type_program = result.printed_schema 19 | 20 | assert array_type_program == expected_array_type_program 21 | end 22 | 23 | test "print array primitive decoder" do 24 | result = 25 | array_type_primitive() 26 | |> ArrayPrinter.print_decoder(schema_def(), %{}, module_name()) 27 | 28 | expected_array_decoder_program = """ 29 | namesDecoder : Decoder (List String) 30 | namesDecoder = 31 | Decode.list Decode.string 32 | """ 33 | 34 | array_decoder_program = result.printed_schema 35 | 36 | assert array_decoder_program == expected_array_decoder_program 37 | end 38 | 39 | test "print array primitive encoder" do 40 | result = 41 | array_type_primitive() 42 | |> ArrayPrinter.print_encoder(schema_def(), %{}, module_name()) 43 | 44 | expected_array_encoder_program = """ 45 | encodeNames : List String -> Value 46 | encodeNames names = 47 | names 48 | |> Encode.list Encode.string 49 | """ 50 | 51 | array_encoder_program = result.printed_schema 52 | 53 | assert array_encoder_program == expected_array_encoder_program 54 | end 55 | 56 | test "print array primitive fuzzer" do 57 | result = 58 | array_type_primitive() 59 | |> ArrayPrinter.print_fuzzer(schema_def(), %{}, module_name()) 60 | 61 | expected_array_fuzzer = """ 62 | namesFuzzer : Fuzzer (List String) 63 | namesFuzzer = 64 | Fuzz.list Fuzz.string 65 | 66 | 67 | encodeDecodeNamesTest : Test 68 | encodeDecodeNamesTest = 69 | fuzz namesFuzzer "can encode and decode Names" <| 70 | \\names -> 71 | names 72 | |> encodeNames 73 | |> Decode.decodeValue namesDecoder 74 | |> Expect.equal (Ok names) 75 | """ 76 | 77 | array_fuzzer = result.printed_schema 78 | 79 | assert array_fuzzer == expected_array_fuzzer 80 | end 81 | 82 | # Array with enum type 83 | 84 | test "print array object type" do 85 | result = 86 | array_type_object() 87 | |> ArrayPrinter.print_type(schema_def(), %{}, module_name()) 88 | 89 | expected_array_type_program = "" 90 | array_type_program = result.printed_schema 91 | 92 | assert array_type_program == expected_array_type_program 93 | end 94 | 95 | test "print array object decoder" do 96 | result = 97 | array_type_object() 98 | |> ArrayPrinter.print_decoder(schema_def(), %{}, module_name()) 99 | 100 | expected_array_decoder_program = """ 101 | colorsDecoder : Decoder (List Color) 102 | colorsDecoder = 103 | Decode.list colorDecoder 104 | """ 105 | 106 | array_decoder_program = result.printed_schema 107 | 108 | assert array_decoder_program == expected_array_decoder_program 109 | end 110 | 111 | test "print array object encoder" do 112 | result = 113 | array_type_object() 114 | |> ArrayPrinter.print_encoder(schema_def(), %{}, module_name()) 115 | 116 | expected_array_encoder_program = """ 117 | encodeColors : List Color -> Value 118 | encodeColors colors = 119 | colors 120 | |> Encode.list encodeColor 121 | """ 122 | 123 | array_encoder_program = result.printed_schema 124 | 125 | assert array_encoder_program == expected_array_encoder_program 126 | end 127 | 128 | test "print array object fuzzer" do 129 | result = 130 | array_type_object() 131 | |> ArrayPrinter.print_fuzzer(schema_def(), %{}, module_name()) 132 | 133 | expected_array_fuzzer = """ 134 | colorsFuzzer : Fuzzer (List Color) 135 | colorsFuzzer = 136 | Fuzz.list colorFuzzer 137 | 138 | 139 | encodeDecodeColorsTest : Test 140 | encodeDecodeColorsTest = 141 | fuzz colorsFuzzer "can encode and decode Colors" <| 142 | \\colors -> 143 | colors 144 | |> encodeColors 145 | |> Decode.decodeValue colorsDecoder 146 | |> Expect.equal (Ok colors) 147 | """ 148 | 149 | array_fuzzer = result.printed_schema 150 | 151 | assert array_fuzzer == expected_array_fuzzer 152 | end 153 | 154 | defp module_name, do: "Domain" 155 | 156 | defp array_type_primitive do 157 | %ArrayType{ 158 | name: "names", 159 | path: URI.parse("#/names"), 160 | items: URI.parse("#/names/items") 161 | } 162 | end 163 | 164 | defp array_type_object do 165 | %ArrayType{ 166 | name: "colors", 167 | path: URI.parse("#/colors"), 168 | items: URI.parse("#/colors/items") 169 | } 170 | end 171 | 172 | defp schema_def do 173 | %SchemaDefinition{ 174 | description: "Test schema", 175 | id: URI.parse("http://example.com/test.json"), 176 | file_path: "test.json", 177 | title: "Test", 178 | types: type_dict() 179 | } 180 | end 181 | 182 | defp type_dict do 183 | %{ 184 | "#/names/items" => %PrimitiveType{ 185 | name: :anonymous, 186 | description: nil, 187 | default: nil, 188 | path: URI.parse("#/names/items"), 189 | type: :string 190 | }, 191 | "#/colors/items" => %EnumType{ 192 | name: "color", 193 | path: URI.parse("#/definitions/color"), 194 | type: "string", 195 | values: ["none", "green", "yellow", "red"] 196 | } 197 | } 198 | end 199 | end 200 | -------------------------------------------------------------------------------- /test/printer/enum_printer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule JS2ETest.Printer.EnumPrinter do 2 | use ExUnit.Case 3 | 4 | require Logger 5 | alias JS2E.Printer 6 | alias JsonSchema.Types 7 | alias Printer.EnumPrinter 8 | alias Types.{EnumType, SchemaDefinition} 9 | 10 | test "print enum type with string values" do 11 | result = 12 | enum_type_with_strings() 13 | |> EnumPrinter.print_type(schema_def(), %{}, module_name()) 14 | 15 | expected_enum_type_program = """ 16 | type Color 17 | = None 18 | | Green 19 | | Yellow 20 | | Red 21 | """ 22 | 23 | enum_type_program = result.printed_schema 24 | 25 | assert enum_type_program == expected_enum_type_program 26 | end 27 | 28 | test "print enum type with number values" do 29 | result = 30 | enum_type_with_numbers() 31 | |> EnumPrinter.print_type(schema_def(), %{}, module_name()) 32 | 33 | expected_enum_type_program = """ 34 | type Temperature 35 | = FloatNeg0_618 36 | | Float1_618 37 | | Float3_14 38 | | Float7_73 39 | """ 40 | 41 | enum_type_program = result.printed_schema 42 | 43 | assert enum_type_program == expected_enum_type_program 44 | end 45 | 46 | test "print enum decoder with string values" do 47 | result = 48 | enum_type_with_strings() 49 | |> EnumPrinter.print_decoder(schema_def(), %{}, module_name()) 50 | 51 | expected_enum_decoder_program = """ 52 | colorDecoder : Decoder Color 53 | colorDecoder = 54 | Decode.string |> Decode.andThen (parseColor >> Decode.fromResult) 55 | 56 | 57 | parseColor : String -> Result String Color 58 | parseColor color = 59 | case color of 60 | "none" -> 61 | Ok None 62 | 63 | "green" -> 64 | Ok Green 65 | 66 | "yellow" -> 67 | Ok Yellow 68 | 69 | "red" -> 70 | Ok Red 71 | 72 | _ -> 73 | Err <| "Unknown color type: " ++ color 74 | """ 75 | 76 | enum_decoder_program = result.printed_schema 77 | 78 | assert enum_decoder_program == expected_enum_decoder_program 79 | end 80 | 81 | test "print enum decoder with number values" do 82 | result = 83 | enum_type_with_numbers() 84 | |> EnumPrinter.print_decoder(schema_def(), %{}, module_name()) 85 | 86 | expected_enum_decoder_program = """ 87 | temperatureDecoder : Decoder Temperature 88 | temperatureDecoder = 89 | Decode.float |> Decode.andThen (parseTemperature >> Decode.fromResult) 90 | 91 | 92 | parseTemperature : Float -> Result String Temperature 93 | parseTemperature temperature = 94 | case temperature of 95 | -0.618 -> 96 | Ok FloatNeg0_618 97 | 98 | 1.618 -> 99 | Ok Float1_618 100 | 101 | 3.14 -> 102 | Ok Float3_14 103 | 104 | 7.73 -> 105 | Ok Float7_73 106 | 107 | _ -> 108 | Err <| "Unknown temperature type: " ++ temperature 109 | """ 110 | 111 | enum_decoder_program = result.printed_schema 112 | 113 | assert enum_decoder_program == expected_enum_decoder_program 114 | end 115 | 116 | test "print enum encoder with string values" do 117 | result = 118 | enum_type_with_strings() 119 | |> EnumPrinter.print_encoder(schema_def(), %{}, module_name()) 120 | 121 | expected_enum_encoder_program = """ 122 | encodeColor : Color -> Value 123 | encodeColor color = 124 | color |> colorToString |> Encode.string 125 | 126 | 127 | colorToString : Color -> String 128 | colorToString color = 129 | case color of 130 | None -> 131 | "none" 132 | 133 | Green -> 134 | "green" 135 | 136 | Yellow -> 137 | "yellow" 138 | 139 | Red -> 140 | "red" 141 | """ 142 | 143 | enum_encoder_program = result.printed_schema 144 | 145 | assert enum_encoder_program == expected_enum_encoder_program 146 | end 147 | 148 | test "print enum encoder with number values" do 149 | result = 150 | enum_type_with_numbers() 151 | |> EnumPrinter.print_encoder(schema_def(), %{}, module_name()) 152 | 153 | expected_enum_encoder_program = """ 154 | encodeTemperature : Temperature -> Value 155 | encodeTemperature temperature = 156 | temperature |> temperatureToFloat |> Encode.float 157 | 158 | 159 | temperatureToFloat : Temperature -> Float 160 | temperatureToFloat temperature = 161 | case temperature of 162 | FloatNeg0_618 -> 163 | -0.618 164 | 165 | Float1_618 -> 166 | 1.618 167 | 168 | Float3_14 -> 169 | 3.14 170 | 171 | Float7_73 -> 172 | 7.73 173 | """ 174 | 175 | enum_encoder_program = result.printed_schema 176 | 177 | assert enum_encoder_program == expected_enum_encoder_program 178 | end 179 | 180 | test "print enum fuzzer with string values" do 181 | result = 182 | enum_type_with_strings() 183 | |> EnumPrinter.print_fuzzer(schema_def(), %{}, module_name()) 184 | 185 | expected_enum_fuzzer_program = """ 186 | colorFuzzer : Fuzzer Color 187 | colorFuzzer = 188 | Fuzz.oneOf 189 | [ Fuzz.constant None 190 | , Fuzz.constant Green 191 | , Fuzz.constant Yellow 192 | , Fuzz.constant Red 193 | ] 194 | 195 | 196 | encodeDecodeColorTest : Test 197 | encodeDecodeColorTest = 198 | fuzz colorFuzzer "can encode and decode Color" <| 199 | \\color -> 200 | color 201 | |> encodeColor 202 | |> Decode.decodeValue colorDecoder 203 | |> Expect.equal (Ok color) 204 | """ 205 | 206 | enum_fuzzer_program = result.printed_schema 207 | 208 | assert enum_fuzzer_program == expected_enum_fuzzer_program 209 | end 210 | 211 | defp module_name, do: "Domain" 212 | 213 | defp enum_type_with_strings, 214 | do: %EnumType{ 215 | name: "color", 216 | path: ["#", "definitions", "color"], 217 | type: :string, 218 | values: ["none", "green", "yellow", "red"] 219 | } 220 | 221 | defp enum_type_with_numbers, 222 | do: %EnumType{ 223 | name: "temperature", 224 | path: ["#", "definitions", "temperature"], 225 | type: :number, 226 | values: [-0.618, 1.618, 3.14, 7.73] 227 | } 228 | 229 | defp schema_def, 230 | do: %SchemaDefinition{ 231 | description: "Test schema", 232 | id: URI.parse("http://example.com/test.json"), 233 | file_path: "test.json", 234 | title: "Test", 235 | types: %{} 236 | } 237 | end 238 | -------------------------------------------------------------------------------- /test/printer/object_printer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule JS2ETest.Printer.ObjectPrinter do 2 | use ExUnit.Case 3 | 4 | require Logger 5 | alias JS2E.Printer 6 | alias JsonSchema.Types 7 | alias Printer.ObjectPrinter 8 | alias Types.{ArrayType, EnumType, ObjectType, PrimitiveType, SchemaDefinition} 9 | 10 | test "print object type" do 11 | result = 12 | object_type() 13 | |> ObjectPrinter.print_type(schema_def(), %{}, module_name()) 14 | 15 | expected_object_type_program = """ 16 | type alias Circle = 17 | { color : Color 18 | , radius : Float 19 | , tags : List String 20 | , title : Maybe String 21 | } 22 | """ 23 | 24 | object_type_program = result.printed_schema 25 | 26 | assert object_type_program == expected_object_type_program 27 | end 28 | 29 | test "print object decoder" do 30 | result = 31 | object_type() 32 | |> ObjectPrinter.print_decoder(schema_def(), %{}, module_name()) 33 | 34 | expected_object_decoder_program = """ 35 | circleDecoder : Decoder Circle 36 | circleDecoder = 37 | Decode.succeed Circle 38 | |> required "color" colorDecoder 39 | |> required "radius" Decode.float 40 | |> required "tags" tagsDecoder 41 | |> optional "title" (Decode.nullable Decode.string) Nothing 42 | """ 43 | 44 | object_decoder_program = result.printed_schema 45 | 46 | assert object_decoder_program == expected_object_decoder_program 47 | end 48 | 49 | test "print object encoder" do 50 | result = 51 | object_type() 52 | |> ObjectPrinter.print_encoder(schema_def(), %{}, module_name()) 53 | 54 | expected_object_encoder_program = """ 55 | encodeCircle : Circle -> Value 56 | encodeCircle circle = 57 | [] 58 | |> Encode.required "color" circle.color encodeColor 59 | |> Encode.required "radius" circle.radius Encode.float 60 | |> Encode.required "tags" circle.tags encodeTags 61 | |> Encode.optional "title" circle.title Encode.string 62 | |> Encode.object 63 | """ 64 | 65 | object_encoder_program = result.printed_schema 66 | 67 | assert object_encoder_program == expected_object_encoder_program 68 | end 69 | 70 | test "print object fuzzer" do 71 | result = 72 | object_type() 73 | |> ObjectPrinter.print_fuzzer(schema_def(), %{}, module_name()) 74 | 75 | expected_object_fuzzer = """ 76 | circleFuzzer : Fuzzer Circle 77 | circleFuzzer = 78 | Fuzz.map4 79 | Circle 80 | colorFuzzer 81 | Fuzz.niceFloat 82 | tagsFuzzer 83 | (Fuzz.maybe Fuzz.string) 84 | 85 | 86 | encodeDecodeCircleTest : Test 87 | encodeDecodeCircleTest = 88 | fuzz circleFuzzer "can encode and decode Circle object" <| 89 | \\circle -> 90 | circle 91 | |> encodeCircle 92 | |> Decode.decodeValue circleDecoder 93 | |> Expect.equal (Ok circle) 94 | """ 95 | 96 | object_fuzzer = result.printed_schema 97 | 98 | assert object_fuzzer == expected_object_fuzzer 99 | end 100 | 101 | defp module_name, do: "Domain" 102 | 103 | defp type_dict, 104 | do: %{ 105 | "#/properties/color" => %EnumType{ 106 | name: "color", 107 | path: URI.parse("#/properties/color"), 108 | type: "string", 109 | default: "none", 110 | values: ["none", "green", "yellow", "red"] 111 | }, 112 | "#/properties/title" => %PrimitiveType{ 113 | name: "title", 114 | path: URI.parse("#/properties/title"), 115 | type: :string 116 | }, 117 | "#/properties/radius" => %PrimitiveType{ 118 | name: "radius", 119 | path: URI.parse("#/properties/radius"), 120 | type: :number 121 | }, 122 | "#/properties/tags" => %ArrayType{ 123 | name: "tags", 124 | path: URI.parse("#/properties/tags"), 125 | items: URI.parse("#/properties/tags/items") 126 | }, 127 | "#/properties/tags/items" => %PrimitiveType{ 128 | name: "items", 129 | path: URI.parse("#/properties/radius/items"), 130 | type: :string 131 | } 132 | } 133 | 134 | defp schema_def, 135 | do: %SchemaDefinition{ 136 | description: "Test schema", 137 | id: URI.parse("http://example.com/test.json"), 138 | file_path: "test.json", 139 | title: "Test", 140 | types: type_dict() 141 | } 142 | 143 | defp object_type, 144 | do: %ObjectType{ 145 | name: "circle", 146 | path: URI.parse("#"), 147 | required: ["color", "radius", "tags"], 148 | properties: %{ 149 | "color" => URI.parse("#/properties/color"), 150 | "title" => URI.parse("#/properties/title"), 151 | "radius" => URI.parse("#/properties/radius"), 152 | "tags" => URI.parse("#/properties/tags") 153 | }, 154 | pattern_properties: %{} 155 | } 156 | end 157 | -------------------------------------------------------------------------------- /test/printer/one_of_printer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule JS2ETest.Printer.OneOfPrinter do 2 | use ExUnit.Case 3 | 4 | require Logger 5 | alias JS2E.Printer 6 | alias JsonSchema.Types 7 | alias Printer.OneOfPrinter 8 | alias Types.{ObjectType, OneOfType, SchemaDefinition, TypeReference} 9 | 10 | test "print 'one of' type value" do 11 | result = 12 | one_of_type() 13 | |> OneOfPrinter.print_type(schema_def(), %{}, module_name()) 14 | 15 | expected_one_of_type_program = """ 16 | type Shape 17 | = ShapeSq Square 18 | | ShapeCi Circle 19 | """ 20 | 21 | one_of_type_program = result.printed_schema 22 | 23 | assert one_of_type_program == expected_one_of_type_program 24 | end 25 | 26 | test "print 'one of' decoder" do 27 | result = 28 | one_of_type() 29 | |> OneOfPrinter.print_decoder(schema_def(), %{}, module_name()) 30 | 31 | expected_one_of_decoder_program = """ 32 | shapeDecoder : Decoder Shape 33 | shapeDecoder = 34 | Decode.oneOf [ squareDecoder |> Decode.map ShapeSq 35 | , circleDecoder |> Decode.map ShapeCi 36 | ] 37 | """ 38 | 39 | one_of_decoder_program = result.printed_schema 40 | 41 | assert one_of_decoder_program == expected_one_of_decoder_program 42 | end 43 | 44 | test "print 'one of' encoder" do 45 | result = 46 | one_of_type() 47 | |> OneOfPrinter.print_encoder(schema_def(), %{}, module_name()) 48 | 49 | expected_one_of_encoder_program = """ 50 | encodeShape : Shape -> Value 51 | encodeShape shape = 52 | case shape of 53 | ShapeSq square -> 54 | encodeSquare square 55 | 56 | ShapeCi circle -> 57 | encodeCircle circle 58 | """ 59 | 60 | one_of_encoder_program = result.printed_schema 61 | 62 | assert one_of_encoder_program == expected_one_of_encoder_program 63 | end 64 | 65 | test "print 'one of' fuzzer" do 66 | result = 67 | one_of_type() 68 | |> OneOfPrinter.print_fuzzer(schema_def(), %{}, module_name()) 69 | 70 | expected_one_of_fuzzer_program = """ 71 | shapeFuzzer : Fuzzer Shape 72 | shapeFuzzer = 73 | Fuzz.oneOf 74 | [ squareFuzzer 75 | , circleFuzzer 76 | ] 77 | 78 | 79 | encodeDecodeShapeTest : Test 80 | encodeDecodeShapeTest = 81 | fuzz shapeFuzzer "can encode and decode Shape" <| 82 | \\shape -> 83 | shape 84 | |> encodeShape 85 | |> Decode.decodeValue shapeDecoder 86 | |> Expect.equal (Ok shape) 87 | """ 88 | 89 | one_of_fuzzer_program = result.printed_schema 90 | 91 | assert one_of_fuzzer_program == expected_one_of_fuzzer_program 92 | end 93 | 94 | defp module_name, do: "Domain" 95 | 96 | defp one_of_type, 97 | do: %OneOfType{ 98 | name: "shape", 99 | path: URI.parse("#/definitions/shape"), 100 | types: [ 101 | URI.parse("#/shape/oneOf/0"), 102 | URI.parse("#/shape/oneOf/1") 103 | ] 104 | } 105 | 106 | defp schema_def, 107 | do: %SchemaDefinition{ 108 | description: "Test schema", 109 | id: URI.parse("http://example.com/test.json"), 110 | file_path: "test.json", 111 | title: "Test", 112 | types: type_dict() 113 | } 114 | 115 | defp type_dict, 116 | do: %{ 117 | "#/shape/oneOf/0" => %TypeReference{ 118 | name: "square", 119 | path: URI.parse("#/definitions/square") 120 | }, 121 | "#/shape/oneOf/1" => %TypeReference{ 122 | name: "circle", 123 | path: URI.parse("#/definitions/circle") 124 | }, 125 | "#/definitions/square" => %ObjectType{ 126 | name: "square", 127 | path: URI.parse("#"), 128 | required: ["color", "size"], 129 | properties: %{ 130 | "color" => URI.parse("#/properties/color"), 131 | "title" => URI.parse("#/properties/size") 132 | }, 133 | pattern_properties: %{} 134 | }, 135 | "#/definitions/circle" => %ObjectType{ 136 | name: "circle", 137 | path: URI.parse("#"), 138 | required: ["color", "radius"], 139 | properties: %{ 140 | "color" => URI.parse("#/properties/color"), 141 | "radius" => URI.parse("#/properties/radius") 142 | }, 143 | pattern_properties: %{} 144 | } 145 | } 146 | end 147 | -------------------------------------------------------------------------------- /test/printer/tuple_printer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule JS2ETest.Printer.TuplePrinter do 2 | use ExUnit.Case 3 | 4 | require Logger 5 | alias JS2E.Printer 6 | alias JsonSchema.Types 7 | alias Printer.TuplePrinter 8 | alias Types.{ObjectType, SchemaDefinition, TupleType, TypeReference} 9 | 10 | test "print 'tuple' type value" do 11 | result = 12 | tuple_type() 13 | |> TuplePrinter.print_type(schema_def(), %{}, module_name()) 14 | 15 | expected_tuple_type_program = """ 16 | type alias ShapePair = 17 | ( Square 18 | , Circle 19 | ) 20 | """ 21 | 22 | tuple_type_program = result.printed_schema 23 | 24 | assert tuple_type_program == expected_tuple_type_program 25 | end 26 | 27 | test "print 'tuple' decoder" do 28 | result = 29 | tuple_type() 30 | |> TuplePrinter.print_decoder(schema_def(), %{}, module_name()) 31 | 32 | expected_tuple_decoder_program = """ 33 | shapePairDecoder : Decoder ShapePair 34 | shapePairDecoder = 35 | Decode.map2 (\\square circle -> (square, circle)) 36 | (Decode.index 0 squareDecoder) 37 | (Decode.index 1 circleDecoder) 38 | """ 39 | 40 | tuple_decoder_program = result.printed_schema 41 | 42 | assert tuple_decoder_program == expected_tuple_decoder_program 43 | end 44 | 45 | test "print 'tuple' encoder" do 46 | result = 47 | tuple_type() 48 | |> TuplePrinter.print_encoder(schema_def(), %{}, module_name()) 49 | 50 | expected_tuple_encoder_program = """ 51 | encodeShapePair : ShapePair -> Value 52 | encodeShapePair (square, circle) = 53 | [] 54 | |> (::) encodeSquare square 55 | |> (::) encodeCircle circle 56 | |> Encode.list 57 | """ 58 | 59 | tuple_encoder_program = result.printed_schema 60 | 61 | assert tuple_encoder_program == expected_tuple_encoder_program 62 | end 63 | 64 | test "print tuple fuzzer" do 65 | result = 66 | tuple_type() 67 | |> TuplePrinter.print_fuzzer(schema_def(), %{}, module_name()) 68 | 69 | expected_tuple_fuzzer = """ 70 | shapePairFuzzer : Fuzzer ShapePair 71 | shapePairFuzzer = 72 | Fuzz.tuple 73 | (squareFuzzer 74 | , circleFuzzer 75 | ) 76 | 77 | 78 | encodeDecodeShapePairTest : Test 79 | encodeDecodeShapePairTest = 80 | fuzz shapePairFuzzer "can encode and decode ShapePair tuple" <| 81 | \\shapePair -> 82 | shapePair 83 | |> encodeShapePair 84 | |> Decode.decodeValue shapePairDecoder 85 | |> Expect.equal (Ok shapePair) 86 | """ 87 | 88 | tuple_fuzzer = result.printed_schema 89 | 90 | assert tuple_fuzzer == expected_tuple_fuzzer 91 | end 92 | 93 | defp module_name, do: "Domain" 94 | 95 | defp tuple_type, 96 | do: %TupleType{ 97 | name: "shapePair", 98 | path: URI.parse("#/shapePair"), 99 | items: [ 100 | URI.parse("#/shapePair/0"), 101 | URI.parse("#/shapePair/1") 102 | ] 103 | } 104 | 105 | defp schema_def, 106 | do: %SchemaDefinition{ 107 | description: "Test schema", 108 | id: URI.parse("http://example.com/test.json"), 109 | file_path: "test.json", 110 | title: "Test", 111 | types: type_dict() 112 | } 113 | 114 | defp type_dict, 115 | do: %{ 116 | "#/shapePair/0" => %TypeReference{ 117 | name: "0", 118 | path: URI.parse("#/definitions/square") 119 | }, 120 | "#/shapePair/1" => %TypeReference{ 121 | name: "1", 122 | path: URI.parse("#/definitions/circle") 123 | }, 124 | "#/definitions/square" => %ObjectType{ 125 | name: "square", 126 | path: URI.parse("#"), 127 | required: ["color", "size"], 128 | properties: %{ 129 | "color" => URI.parse("#/properties/color"), 130 | "title" => URI.parse("#/properties/size") 131 | }, 132 | pattern_properties: %{} 133 | }, 134 | "#/definitions/circle" => %ObjectType{ 135 | name: "circle", 136 | path: ["#"], 137 | required: ["color", "radius"], 138 | properties: %{ 139 | "color" => URI.parse("#/properties/color"), 140 | "radius" => URI.parse("#/properties/radius") 141 | }, 142 | pattern_properties: %{} 143 | } 144 | } 145 | end 146 | -------------------------------------------------------------------------------- /test/printer/union_printer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule JS2ETest.Printer.UnionPrinter do 2 | use ExUnit.Case 3 | 4 | require Logger 5 | alias JS2E.Printer 6 | alias JsonSchema.Types 7 | alias Printer.UnionPrinter 8 | alias Types.{SchemaDefinition, UnionType} 9 | 10 | test "print union type value" do 11 | result = 12 | union_type() 13 | |> UnionPrinter.print_type(schema_def(), %{}, module_name()) 14 | 15 | expected_union_type_program = """ 16 | type FavoriteNumber 17 | = FavoriteNumber_F Float 18 | | FavoriteNumber_I Int 19 | """ 20 | 21 | union_type_program = result.printed_schema 22 | 23 | assert union_type_program == expected_union_type_program 24 | end 25 | 26 | test "print union type with null value" do 27 | result = 28 | union_type_with_null() 29 | |> UnionPrinter.print_type(schema_def(), %{}, module_name()) 30 | 31 | expected_union_type_program = """ 32 | type FavoriteNumber 33 | = FavoriteNumber_F Float 34 | | FavoriteNumber_I Int 35 | """ 36 | 37 | union_type_program = result.printed_schema 38 | 39 | assert union_type_program == expected_union_type_program 40 | end 41 | 42 | test "print union decoder" do 43 | result = 44 | union_type() 45 | |> UnionPrinter.print_decoder(schema_def(), %{}, module_name()) 46 | 47 | expected_union_decoder_program = """ 48 | favoriteNumberDecoder : Decoder FavoriteNumber 49 | favoriteNumberDecoder = 50 | Decode.oneOf [ Decode.float |> Decode.map FavoriteNumber_F 51 | , Decode.int |> Decode.map FavoriteNumber_I 52 | ] 53 | """ 54 | 55 | union_decoder_program = result.printed_schema 56 | 57 | assert union_decoder_program == expected_union_decoder_program 58 | end 59 | 60 | test "print union decoder with null value" do 61 | result = 62 | union_type_with_null() 63 | |> UnionPrinter.print_decoder(schema_def(), %{}, module_name()) 64 | 65 | expected_union_decoder_program = """ 66 | favoriteNumberDecoder : Decoder (Maybe FavoriteNumber) 67 | favoriteNumberDecoder = 68 | Decode.oneOf [ Decode.float |> Decode.map (FavoriteNumber_F >> Just) 69 | , Decode.int |> Decode.map (FavoriteNumber_I >> Just) 70 | , Decode.null Nothing 71 | ] 72 | """ 73 | 74 | union_decoder_program = result.printed_schema 75 | 76 | assert union_decoder_program == expected_union_decoder_program 77 | end 78 | 79 | test "print union encoder" do 80 | result = 81 | union_type() 82 | |> UnionPrinter.print_encoder(schema_def(), %{}, module_name()) 83 | 84 | expected_union_encoder_program = """ 85 | encodeFavoriteNumber : FavoriteNumber -> Value 86 | encodeFavoriteNumber favoriteNumber = 87 | case favoriteNumber of 88 | FavoriteNumber_F floatValue -> 89 | Encode.float floatValue 90 | 91 | FavoriteNumber_I intValue -> 92 | Encode.int intValue 93 | """ 94 | 95 | union_encoder_program = result.printed_schema 96 | 97 | assert union_encoder_program == expected_union_encoder_program 98 | end 99 | 100 | test "print union fuzzer" do 101 | result = 102 | union_type() 103 | |> UnionPrinter.print_fuzzer(schema_def(), %{}, module_name()) 104 | 105 | expected_union_fuzzer = """ 106 | favoriteNumberFuzzer : Fuzzer FavoriteNumber 107 | favoriteNumberFuzzer = 108 | Fuzz.oneOf 109 | [ Fuzz.map FavoriteNumber_F Fuzz.niceFloat 110 | , Fuzz.map FavoriteNumber_I Fuzz.int 111 | ] 112 | 113 | 114 | encodeDecodeFavoriteNumberTest : Test 115 | encodeDecodeFavoriteNumberTest = 116 | fuzz favoriteNumberFuzzer "can encode and decode FavoriteNumber" <| 117 | \\favoriteNumber -> 118 | favoriteNumber 119 | |> encodeFavoriteNumber 120 | |> Decode.decodeValue favoriteNumberDecoder 121 | |> Expect.equal (Ok favoriteNumber) 122 | """ 123 | 124 | union_fuzzer = result.printed_schema 125 | 126 | assert union_fuzzer == expected_union_fuzzer 127 | end 128 | 129 | defp module_name, do: "Domain" 130 | 131 | defp schema_def, 132 | do: %SchemaDefinition{ 133 | description: "Test schema", 134 | id: URI.parse("http://example.com/test.json"), 135 | file_path: "test.json", 136 | title: "Test", 137 | types: %{} 138 | } 139 | 140 | defp union_type, 141 | do: %UnionType{ 142 | name: "favoriteNumber", 143 | path: ["#", "definitions", "favoriteNumber"], 144 | types: [:number, :integer] 145 | } 146 | 147 | defp union_type_with_null, 148 | do: %UnionType{ 149 | name: "favoriteNumber", 150 | path: ["#", "definitions", "favoriteNumber"], 151 | types: [:number, :integer, :null] 152 | } 153 | end 154 | -------------------------------------------------------------------------------- /test/printer/utils/common_operations_test.exs: -------------------------------------------------------------------------------- 1 | defmodule JS2ETest.Printer.Utils.CommonOperations do 2 | use ExUnit.Case, async: true 3 | doctest JS2E.Printer.Utils.CommonOperations, import: true 4 | end 5 | -------------------------------------------------------------------------------- /test/printer/utils/elm_decoders_test.exs: -------------------------------------------------------------------------------- 1 | defmodule JS2ETest.Printer.Utils.ElmDecoders do 2 | use ExUnit.Case, async: true 3 | doctest JS2E.Printer.Utils.ElmDecoders, import: true 4 | end 5 | -------------------------------------------------------------------------------- /test/printer/utils/elm_encoders_test.exs: -------------------------------------------------------------------------------- 1 | defmodule JS2ETest.Printer.Utils.ElmEncoders do 2 | use ExUnit.Case, async: true 3 | doctest JS2E.Printer.Utils.ElmEncoders, import: true 4 | end 5 | -------------------------------------------------------------------------------- /test/printer/utils/elm_fuzzers_test.exs: -------------------------------------------------------------------------------- 1 | defmodule JS2ETest.Printer.Utils.ElmFuzzers do 2 | use ExUnit.Case, async: true 3 | doctest JS2E.Printer.Utils.ElmFuzzers, import: true 4 | end 5 | -------------------------------------------------------------------------------- /test/printer/utils/elm_types_test.exs: -------------------------------------------------------------------------------- 1 | defmodule JS2ETest.Printer.Utils.ElmTypes do 2 | use ExUnit.Case, async: true 3 | doctest JS2E.Printer.Utils.ElmTypes, import: true 4 | end 5 | -------------------------------------------------------------------------------- /test/printer/utils/indentation_test.exs: -------------------------------------------------------------------------------- 1 | defmodule JS2ETest.Printer.Utils.Indentation do 2 | use ExUnit.Case, async: true 3 | doctest JS2E.Printer.Utils.Indentation, import: true 4 | end 5 | -------------------------------------------------------------------------------- /test/printer/utils/naming_test.exs: -------------------------------------------------------------------------------- 1 | defmodule JS2ETest.Printer.Utils.Naming do 2 | use ExUnit.Case, async: true 3 | doctest JS2E.Printer.Utils.Naming, import: true 4 | end 5 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------