├── .credo.exs ├── .formatter.exs ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .tool-versions ├── LICENSE.txt ├── README.md ├── lib ├── openapi_codegen.ex └── openapi_codegen │ ├── ast.ex │ ├── cli.ex │ ├── client │ ├── query_param.ex │ ├── req │ │ ├── client.ex │ │ └── path.ex │ ├── request_body.ex │ └── tesla │ │ ├── client.ex │ │ └── path.ex │ └── components.ex ├── mix.exs ├── mix.lock └── test ├── integration_test.exs ├── openapi_codegen_test.exs ├── support └── fixtures │ ├── expected │ ├── req │ │ ├── PetStore.ex │ │ └── components │ │ │ ├── Address.ex │ │ │ ├── ApiResponse.ex │ │ │ ├── Category.ex │ │ │ ├── Customer.ex │ │ │ ├── Order.ex │ │ │ ├── Pet.ex │ │ │ ├── Tag.ex │ │ │ └── User.ex │ └── tesla │ │ ├── PetStore.ex │ │ └── components │ │ ├── Address.ex │ │ ├── ApiResponse.ex │ │ ├── Category.ex │ │ ├── Customer.ex │ │ ├── Order.ex │ │ ├── Pet.ex │ │ ├── Tag.ex │ │ └── User.ex │ ├── openapi_petstore.json │ └── petstore │ ├── openapi_v31.json │ ├── openapi_v31.yaml │ └── openapi_v31.yml └── test_helper.exs /.credo.exs: -------------------------------------------------------------------------------- 1 | %{ 2 | configs: [ 3 | %{ 4 | # 5 | # Run any config using `mix credo -C `. If no config name is given 6 | # "default" is used. 7 | name: "default", 8 | # 9 | files: %{ 10 | included: [ 11 | "**/*.{ex,exs}" 12 | ], 13 | excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] 14 | }, 15 | plugins: [], 16 | requires: [], 17 | strict: false, 18 | parse_timeout: 5000, 19 | color: true, 20 | checks: %{ 21 | enabled: [ 22 | # 23 | ## Consistency Checks 24 | # 25 | {Credo.Check.Consistency.ExceptionNames, []}, 26 | {Credo.Check.Consistency.LineEndings, []}, 27 | {Credo.Check.Consistency.ParameterPatternMatching, false}, 28 | {Credo.Check.Consistency.SpaceAroundOperators, []}, 29 | {Credo.Check.Consistency.SpaceInParentheses, []}, 30 | {Credo.Check.Consistency.TabsOrSpaces, []}, 31 | 32 | # 33 | ## Design Checks 34 | # 35 | # You can customize the priority of any check 36 | # Priority values are: `low, normal, high, higher` 37 | # 38 | {Credo.Check.Design.AliasUsage, [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, 39 | # You can also customize the exit_status of each check. 40 | # If you don't want TODO comments to cause `mix credo` to fail, just 41 | # set this value to 0 (zero). 42 | # 43 | {Credo.Check.Design.TagTODO, [exit_status: 0]}, 44 | {Credo.Check.Design.TagFIXME, [exit_status: 0]}, 45 | 46 | # 47 | ## Readability Checks 48 | # 49 | {Credo.Check.Readability.AliasOrder, false}, 50 | {Credo.Check.Readability.FunctionNames, []}, 51 | {Credo.Check.Readability.LargeNumbers, false}, 52 | {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, 53 | {Credo.Check.Readability.ModuleAttributeNames, []}, 54 | {Credo.Check.Readability.ModuleDoc, false}, 55 | {Credo.Check.Readability.ModuleNames, []}, 56 | {Credo.Check.Readability.ParenthesesInCondition, []}, 57 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, 58 | {Credo.Check.Readability.PipeIntoAnonymousFunctions, false}, 59 | {Credo.Check.Readability.PredicateFunctionNames, []}, 60 | {Credo.Check.Readability.PreferImplicitTry, []}, 61 | {Credo.Check.Readability.RedundantBlankLines, []}, 62 | {Credo.Check.Readability.Semicolons, []}, 63 | {Credo.Check.Readability.SpaceAfterCommas, []}, 64 | {Credo.Check.Readability.StringSigils, []}, 65 | {Credo.Check.Readability.TrailingBlankLine, []}, 66 | {Credo.Check.Readability.TrailingWhiteSpace, []}, 67 | {Credo.Check.Readability.UnnecessaryAliasExpansion, false}, 68 | {Credo.Check.Readability.VariableNames, []}, 69 | {Credo.Check.Readability.WithSingleClause, []}, 70 | 71 | # 72 | ## Refactoring Opportunities 73 | # 74 | {Credo.Check.Refactor.Apply, []}, 75 | {Credo.Check.Refactor.CondStatements, []}, 76 | {Credo.Check.Refactor.CyclomaticComplexity, []}, 77 | {Credo.Check.Refactor.FunctionArity, []}, 78 | {Credo.Check.Refactor.LongQuoteBlocks, []}, 79 | {Credo.Check.Refactor.MatchInCondition, []}, 80 | {Credo.Check.Refactor.MapJoin, []}, 81 | {Credo.Check.Refactor.NegatedConditionsInUnless, []}, 82 | {Credo.Check.Refactor.NegatedConditionsWithElse, []}, 83 | {Credo.Check.Refactor.Nesting, []}, 84 | {Credo.Check.Refactor.UnlessWithElse, []}, 85 | {Credo.Check.Refactor.WithClauses, []}, 86 | {Credo.Check.Refactor.FilterCount, []}, 87 | {Credo.Check.Refactor.FilterFilter, []}, 88 | {Credo.Check.Refactor.RejectReject, []}, 89 | {Credo.Check.Refactor.RedundantWithClauseResult, []}, 90 | 91 | # 92 | ## Warnings 93 | # 94 | {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, 95 | {Credo.Check.Warning.BoolOperationOnSameValues, []}, 96 | {Credo.Check.Warning.Dbg, []}, 97 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, 98 | {Credo.Check.Warning.IExPry, []}, 99 | {Credo.Check.Warning.IoInspect, []}, 100 | {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []}, 101 | {Credo.Check.Warning.OperationOnSameValues, []}, 102 | {Credo.Check.Warning.OperationWithConstantResult, []}, 103 | {Credo.Check.Warning.RaiseInsideRescue, []}, 104 | {Credo.Check.Warning.SpecWithStruct, []}, 105 | {Credo.Check.Warning.WrongTestFileExtension, []}, 106 | {Credo.Check.Warning.UnusedEnumOperation, []}, 107 | {Credo.Check.Warning.UnusedFileOperation, []}, 108 | {Credo.Check.Warning.UnusedKeywordOperation, []}, 109 | {Credo.Check.Warning.UnusedListOperation, []}, 110 | {Credo.Check.Warning.UnusedPathOperation, []}, 111 | {Credo.Check.Warning.UnusedRegexOperation, []}, 112 | {Credo.Check.Warning.UnusedStringOperation, []}, 113 | {Credo.Check.Warning.UnusedTupleOperation, []}, 114 | {Credo.Check.Warning.UnsafeExec, []} 115 | ], 116 | disabled: [ 117 | # 118 | # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) 119 | 120 | # 121 | # Controversial and experimental checks (opt-in, just move the check to `:enabled` 122 | # and be sure to use `mix credo --strict` to see low priority checks) 123 | # 124 | {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, 125 | {Credo.Check.Consistency.UnusedVariableNames, []}, 126 | {Credo.Check.Design.DuplicatedCode, []}, 127 | {Credo.Check.Design.SkipTestWithoutComment, []}, 128 | {Credo.Check.Readability.AliasAs, []}, 129 | {Credo.Check.Readability.BlockPipe, []}, 130 | {Credo.Check.Readability.ImplTrue, []}, 131 | {Credo.Check.Readability.MultiAlias, []}, 132 | {Credo.Check.Readability.NestedFunctionCalls, []}, 133 | {Credo.Check.Readability.OneArityFunctionInPipe, []}, 134 | {Credo.Check.Readability.SeparateAliasRequire, []}, 135 | {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, 136 | {Credo.Check.Readability.SinglePipe, []}, 137 | {Credo.Check.Readability.Specs, []}, 138 | {Credo.Check.Readability.StrictModuleLayout, []}, 139 | {Credo.Check.Readability.WithCustomTaggedTuple, []}, 140 | {Credo.Check.Readability.OnePipePerLine, []}, 141 | {Credo.Check.Refactor.ABCSize, []}, 142 | {Credo.Check.Refactor.AppendSingleItem, []}, 143 | {Credo.Check.Refactor.DoubleBooleanNegation, []}, 144 | {Credo.Check.Refactor.FilterReject, []}, 145 | {Credo.Check.Refactor.IoPuts, []}, 146 | {Credo.Check.Refactor.MapMap, []}, 147 | {Credo.Check.Refactor.ModuleDependencies, []}, 148 | {Credo.Check.Refactor.NegatedIsNil, []}, 149 | {Credo.Check.Refactor.PassAsyncInTestCases, []}, 150 | {Credo.Check.Refactor.PipeChainStart, []}, 151 | {Credo.Check.Refactor.RejectFilter, []}, 152 | {Credo.Check.Refactor.VariableRebinding, []}, 153 | {Credo.Check.Warning.LazyLogging, []}, 154 | {Credo.Check.Warning.LeakyEnvironment, []}, 155 | {Credo.Check.Warning.MapGetUnsafePass, []}, 156 | {Credo.Check.Warning.MixEnv, []}, 157 | {Credo.Check.Warning.UnsafeToAtom, []} 158 | 159 | # {Credo.Check.Refactor.MapInto, []}, 160 | 161 | # 162 | # Custom checks can be created using `mix credo.gen.check`. 163 | # 164 | ] 165 | } 166 | } 167 | ] 168 | } 169 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | plugins: [Styler], 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | types: [opened, synchronize] 9 | 10 | jobs: 11 | test: 12 | strategy: 13 | matrix: 14 | os: [ubuntu-22.04] 15 | otp: [26.x] 16 | elixir: [1.16.x] 17 | 18 | name: OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} (${{ matrix.os }}) 19 | runs-on: ${{ matrix.os }} 20 | 21 | steps: 22 | - name: ☁️ Checkout repository 23 | uses: actions/checkout@v3 24 | 25 | - name: 💧 Setup Elixir ${{ matrix.elixir }} (OTP ${{matrix.otp}}) 26 | uses: marmelasoft/elixir-setup@main 27 | with: 28 | otp-version: ${{ matrix.otp }} 29 | elixir-version: ${{ matrix.elixir }} 30 | build-flags: --all-warnings --warnings-as-errors 31 | env: 32 | MIX_ENV: test 33 | 34 | - name: 🔬 Run the tests 35 | run: mix test --warnings-as-errors 36 | env: 37 | MIX_ENV: test 38 | 39 | # # Cache key based on Erlang/Elixir version and the mix.lock hash 40 | # - name: Restore PLT cache 41 | # id: plt_cache 42 | # uses: actions/cache/restore@v3 43 | # with: 44 | # key: | 45 | # plt-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }} 46 | # restore-keys: | 47 | # plt-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}- 48 | # path: | 49 | # priv/plts 50 | 51 | # # Create PLTs if no cache was found 52 | # - name: Create PLTs 53 | # if: steps.plt_cache.outputs.cache-hit != 'true' 54 | # run: mix dialyzer --plt 55 | 56 | # # By default, the GitHub Cache action will only save the cache if all steps in the job succeed, 57 | # # so we separate the cache restore and save steps in case running dialyzer fails. 58 | # - name: Save PLT cache 59 | # id: plt_cache_save 60 | # uses: actions/cache/save@v3 61 | # if: steps.plt_cache.outputs.cache-hit != 'true' 62 | # with: 63 | # key: | 64 | # plt-${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-${{ hashFiles('**/mix.lock') }} 65 | # path: | 66 | # priv/plts 67 | 68 | # - name: 🔍 Analyze the code 69 | # run: mix dialyzer --format github 70 | 71 | - name: 🧹 Check code formating 72 | run: mix format --check-formatted 73 | if: always() 74 | 75 | - name: 💡 Lint the code 76 | run: mix credo --strict --all 77 | if: always() 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | openapi_codegen 2 | 3 | # The directory Mix will write compiled artifacts to. 4 | /_build/ 5 | 6 | # If you run "mix test --cover", coverage assets end up here. 7 | /cover/ 8 | 9 | # The directory Mix downloads your dependencies sources to. 10 | /deps/ 11 | 12 | # Where third-party dependencies like ExDoc output generated docs. 13 | /doc/ 14 | 15 | # Ignore .fetch files in case you like to edit your project deps locally. 16 | /.fetch 17 | 18 | # If the VM crashes, it generates a dump, let's ignore it too. 19 | erl_crash.dump 20 | 21 | # Also ignore archive artifacts (built via "mix archive.build"). 22 | *.ez 23 | 24 | # Ignore package tarball (built via "mix hex.build"). 25 | openapi_codegen-*.tar 26 | 27 | # Temporary files, for example, from tests. 28 | /tmp/ 29 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.16.0-otp-26 2 | erlang 26.2.1 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright © 2023 Marmelasoft 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the “Software”), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenAPI Code Generation 2 | 3 | Code generator that ingest a OpenAPI Specification and writes the structs that represent resources of that given specification and also builds the client based on the paths. 4 | 5 | Currently supports two types of generation: 6 | 7 | * [Tesla](https://hexdocs.pm/tesla/readme.html) - Generates a module with a Tesla client. 8 | * [Req](https://hexdocs.pm/req/readme.html) - Generates a module using Req. 9 | 10 | By default we're using Jason for encoding and decoding of structs 11 | 12 | Does not require you to install extra dependencies as we're using 13 | 14 | ## Roadmap 15 | 16 | - [x] Support YAML specification parsing 17 | - [ ] Support Inheritance and Polymorphism 18 | - [ ] Add typespecs to generated Structs 19 | - [ ] Add typespecs to Client 20 | - [ ] Generate tests for Client 21 | 22 | ## Installation 23 | 24 | Install it using: 25 | 26 | ```sh 27 | mix do local.rebar --force, local.hex --force 28 | mix escript.install hex openapi_codegen 29 | ``` 30 | 31 | ## Usage 32 | 33 | Generating a Tesla client using PetStore example: 34 | 35 | `openapi_codegen --tesla --output-path lib openapi_petstore.json` 36 | 37 | Generating a Req client using PetStore example: 38 | 39 | `openapi_codegen --req --output-path lib openapi_petstore.json` 40 | 41 | This examples will generate your code in your folder `lib` and also create a `lib/components` with all the structs. 42 | 43 | Learn more on how to use it with `openapi_codegen --help`. 44 | -------------------------------------------------------------------------------- /lib/openapi_codegen.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenApiCodeGen do 2 | @moduledoc """ 3 | Main module that coordinates the code generation. 4 | """ 5 | alias OpenApiCodeGen.Ast 6 | alias OpenApiCodeGen.Client.Req 7 | alias OpenApiCodeGen.Client.Tesla 8 | alias OpenApiCodeGen.Components 9 | 10 | @spec generate(Path.t(), map(), :req | :tesla) :: %{schemas: list(Path.t()), client: Path.t()} 11 | def generate(path, spec, adapter) when is_map(spec) do 12 | name = 13 | path 14 | |> Path.split() 15 | |> Enum.take(-1) 16 | |> hd() 17 | |> Macro.camelize() 18 | 19 | schemas = Components.generate(name, spec) 20 | 21 | client = 22 | case adapter do 23 | :req -> Req.generate(name, spec) 24 | :tesla -> Tesla.generate(name, spec) 25 | end 26 | 27 | client_file_path = Ast.to_file!(client, name, path) 28 | 29 | schema_file_paths = 30 | Enum.map(schemas, fn {component_name, ast} -> 31 | Ast.to_file!(ast, component_name, Path.join(path, "components")) 32 | end) 33 | 34 | %{schemas: schema_file_paths, client: client_file_path} 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/openapi_codegen/ast.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenApiCodeGen.Ast do 2 | @moduledoc """ 3 | Contains functions to help with Ast operations. 4 | """ 5 | 6 | @doc """ 7 | Converts the AST into a string, formats it, styles it and writes it to a file and returns the path to the file. 8 | """ 9 | @spec to_file!(Macro.t(), String.t(), Path.t()) :: Path.t() 10 | def to_file!(ast, key, path) do 11 | File.mkdir_p!(path) 12 | 13 | file_path = path |> Path.join("#{key}.ex") |> String.replace("-", "_") 14 | 15 | ast 16 | |> Macro.to_string() 17 | |> tap(&File.write!(file_path, &1)) 18 | |> Code.format_string!() 19 | |> Enum.join() 20 | |> Styler.format([]) 21 | |> then(&File.write!(file_path, &1)) 22 | 23 | file_path 24 | end 25 | 26 | @reserved_words ~w(do end else catch rescue after alias and or not when fn quote unquote unquote_splicing) 27 | @doc """ 28 | Converts a string or atom into a variable. 29 | 30 | ## Examples 31 | 32 | iex> to_var(:my_var, __MODULE__) 33 | {:my_var, [], __MODULE__} 34 | 35 | iex> to_var("my_var", __MODULE__) 36 | {:my_var, [], __MODULE__} 37 | 38 | iex> to_var("myVar", __MODULE__) 39 | {:my_var, [], __MODULE__} 40 | """ 41 | @spec to_var(binary() | atom(), atom()) :: Macro.t() 42 | def to_var(name, context) when is_atom(name) do 43 | name 44 | |> Atom.to_string() 45 | |> to_var(context) 46 | end 47 | 48 | def to_var(name, context) when is_binary(name) do 49 | name 50 | |> sanitize_name() 51 | |> Macro.var(context) 52 | end 53 | 54 | @doc """ 55 | Converts a string or atom into a variable. It also checks for reserved words and appends `_param` to the variable name if it is a reserved word. 56 | """ 57 | @spec sanitize_name(binary, :camelize | :underscore) :: atom 58 | def sanitize_name(name, transform \\ :underscore) do 59 | name = 60 | name 61 | |> String.replace(~r/\W/, "_") 62 | |> String.replace(~r/^\d/, "Component\\0") 63 | |> then( 64 | &case transform do 65 | :camelize -> Macro.camelize(&1) 66 | :underscore -> Macro.underscore(&1) 67 | end 68 | ) 69 | 70 | @reserved_words 71 | |> Enum.find(&(&1 == name)) 72 | |> then(fn 73 | nil -> 74 | name 75 | 76 | _ -> 77 | case transform do 78 | :camelize -> Macro.camelize("#{name}Param") 79 | :underscore -> Macro.underscore("#{name}_param") 80 | end 81 | end) 82 | |> String.to_atom() 83 | end 84 | 85 | @path_elements_pattern ~r/{([^}]*)}/ 86 | @doc """ 87 | Generates an interpolated string given a URL path. 88 | """ 89 | def generate_path_interpolation(client_module_name, path) do 90 | @path_elements_pattern 91 | |> Regex.split(path, include_captures: true) 92 | |> Enum.reject(&(&1 == "")) 93 | |> Enum.map(fn path -> 94 | case Regex.run(@path_elements_pattern, path) do 95 | [_, path] -> 96 | path 97 | |> to_var(client_module_name) 98 | |> then("e(do: :"Elixir.Kernel".to_string(unquote(&1)) :: binary)) 99 | 100 | _ -> 101 | quote(do: unquote(path)) 102 | end 103 | end) 104 | |> then(&{:<<>>, [], &1}) 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/openapi_codegen/cli.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenApiCodeGen.CLI do 2 | @moduledoc """ 3 | Usage: openapi_codegen [options] INPUT_FILE 4 | 5 | Available options: 6 | 7 | --output-path Output directory where the code should be generated. 8 | --req To use the :req as HTTP client. 9 | --tesla To use the :tesla as HTTP client (default). 10 | 11 | The --help and --version options can be given instead of a command for usage and versioning information. 12 | """ 13 | 14 | def usage, do: @moduledoc 15 | 16 | def main([arg]) when arg in ["--help", "-h"], do: display_help() 17 | def main([arg]) when arg in ["--version", "-v"], do: display_version() 18 | 19 | def main(argv) do 20 | {opts, [openapi_spec_path | _extra_args]} = args_to_options(argv) 21 | 22 | output_path = opts[:output_path] 23 | use_req? = opts[:req] 24 | 25 | if use_req? do 26 | OpenApiCodeGen.generate(output_path, read_spec_file!(openapi_spec_path), :req) 27 | else 28 | OpenApiCodeGen.generate(output_path, read_spec_file!(openapi_spec_path), :tesla) 29 | end 30 | end 31 | 32 | defp read_spec_file!(file_path) do 33 | content = File.read!(file_path) 34 | 35 | case Path.extname(file_path) do 36 | ".json" -> 37 | Jason.decode!(content) 38 | 39 | ".yml" -> 40 | YamlElixir.read_from_string!(content) 41 | 42 | ".yaml" -> 43 | YamlElixir.read_from_string!(content) 44 | 45 | ext -> 46 | raise "file extension #{ext} not supported" 47 | end 48 | end 49 | 50 | @switches [ 51 | output_path: :string, 52 | req: :boolean, 53 | tesla: :boolean 54 | ] 55 | 56 | @aliases [ 57 | o: :output_path 58 | ] 59 | 60 | defp args_to_options(args) do 61 | {opts, extra_args} = OptionParser.parse!(args, strict: @switches, aliases: @aliases) 62 | validate_options!(opts) 63 | validate_extra_args!(extra_args) 64 | {opts, extra_args} 65 | end 66 | 67 | defp validate_options!(opts) do 68 | cond do 69 | Keyword.has_key?(opts, :tesla) and Keyword.has_key?(opts, :req) -> 70 | raise "the provided --req and --tesla options are mutually exclusive, please specify only one of them" 71 | 72 | not Keyword.has_key?(opts, :output_path) -> 73 | raise "--output_path is required" 74 | 75 | true -> 76 | nil 77 | end 78 | end 79 | 80 | defp validate_extra_args!(extra_args) do 81 | if extra_args == [] do 82 | raise "open api spec path is required" 83 | end 84 | end 85 | 86 | defp display_help do 87 | IO.puts("OpenApiCodeGen is a Code Generation tool for Elixir\n") 88 | IO.write(usage()) 89 | end 90 | 91 | defp display_version do 92 | IO.puts(:erlang.system_info(:system_version)) 93 | IO.puts("Elixir " <> System.build_info()[:build]) 94 | 95 | version = Application.spec(:openapi_codegen, :vsn) 96 | IO.puts("\nOpenApiCodeGen #{version}") 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/openapi_codegen/client/query_param.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenApiCodeGen.Client.QueryParam do 2 | @moduledoc """ 3 | Generates query param AST from OpenAPI spec using the `parameters` key. 4 | """ 5 | alias OpenApiCodeGen.Ast 6 | 7 | @doc """ 8 | Generates query param AST from OpenAPI spec using the `parameters` key. 9 | Returns the variable and the keyword list elements to be used. 10 | """ 11 | @spec generate(atom(), map()) :: Keyword.t() 12 | def generate(name, %{"parameters" => parameters}), 13 | do: 14 | parameters 15 | |> Enum.flat_map(&generate_url_parameter(name, &1)) 16 | |> Enum.reduce([], &Keyword.merge/2) 17 | |> Enum.reverse() 18 | 19 | def generate(_, _), do: [] 20 | 21 | defp generate_url_parameter(name, %{"in" => "query"} = parameter), do: parameter_to_ast(name, parameter) 22 | 23 | defp generate_url_parameter(_, _), do: [] 24 | 25 | defp parameter_to_ast(name, %{"name" => param_name}) do 26 | var_name = 27 | param_name 28 | |> Macro.underscore() 29 | |> String.to_atom() 30 | 31 | var = Ast.to_var(var_name, name) 32 | 33 | [quote(do: [{unquote(var_name), unquote(var)}])] 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/openapi_codegen/client/req/client.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenApiCodeGen.Client.Req do 2 | @moduledoc """ 3 | Req client code generation. 4 | """ 5 | 6 | alias OpenApiCodeGen.Ast 7 | alias OpenApiCodeGen.Client.QueryParam 8 | alias OpenApiCodeGen.Client.Req.Path 9 | alias OpenApiCodeGen.Client.RequestBody 10 | 11 | @path_elements_pattern ~r/{([^}]*)}/ 12 | 13 | @doc """ 14 | Generates client AST from OpenAPI spec using: 15 | 16 | ## Params 17 | 18 | * `servers` to generate Tesla middleware for the base URL 19 | * `operationId` for the name of the functions 20 | * `paths` keys to determine method for the function 21 | * `requestBody` to generate the request body argument 22 | * `path` to generate the request path with string interpolation 23 | * `parameters` to generate the URL parameters (if they are of type `query`) 24 | """ 25 | @spec generate(String.t(), map()) :: Macro.t() 26 | def generate(name, %{"paths" => paths, "servers" => servers}) do 27 | case servers do 28 | [%{"url" => server} | _] -> build_client_ast(name, paths, server) 29 | _ -> build_client_ast(name, paths, "") 30 | end 31 | end 32 | 33 | defp build_client_ast(name, paths, server) do 34 | client_module_name = String.to_atom("Elixir.#{name}") 35 | 36 | quote do 37 | defmodule unquote(client_module_name) do 38 | @req Req.new(base_url: unquote(server)) 39 | 40 | unquote_splicing(generate_functions_ast(client_module_name, paths)) 41 | end 42 | end 43 | end 44 | 45 | defp generate_functions_ast(client_module_name, paths), do: Enum.map(paths, &generate_function(client_module_name, &1)) 46 | 47 | defp generate_function(client_module_name, spec) do 48 | case spec do 49 | {path, %{"get" => content}} -> generate_function(client_module_name, path, content, :get) 50 | {path, %{"post" => content}} -> generate_function(client_module_name, path, content, :post) 51 | {path, %{"put" => content}} -> generate_function(client_module_name, path, content, :put) 52 | {path, %{"delete" => content}} -> generate_function(client_module_name, path, content, :delete) 53 | {path, %{"patch" => content}} -> generate_function(client_module_name, path, content, :patch) 54 | end 55 | end 56 | 57 | defp generate_function(client_module_name, path, %{"operationId" => func_name} = schema, method) do 58 | request_body_arguments = RequestBody.generate(client_module_name, schema) 59 | url_parameters = QueryParam.generate(client_module_name, schema) 60 | request_path = Path.generate(client_module_name, path) 61 | 62 | function_arguments = 63 | generate_function_arguments(client_module_name, path, request_body_arguments, url_parameters) 64 | 65 | build_request_function_ast( 66 | client_module_name, 67 | func_name, 68 | method, 69 | request_path, 70 | function_arguments, 71 | request_body_arguments, 72 | url_parameters 73 | ) 74 | end 75 | 76 | defp generate_function_arguments(client_module_name, path, request_body_arguments, url_parameters) do 77 | path_arguments = 78 | @path_elements_pattern 79 | |> Regex.scan(path) 80 | |> Enum.map(fn [_, arg] -> Ast.to_var(arg, client_module_name) end) 81 | 82 | path_arguments 83 | |> maybe_append_request_body_function_argument(request_body_arguments) 84 | |> maybe_append_url_parameters_function_arguments(url_parameters, client_module_name) 85 | end 86 | 87 | defp maybe_append_request_body_function_argument(function_arguments, request_body_arguments) do 88 | case request_body_arguments do 89 | nil -> function_arguments 90 | {_, ast} -> function_arguments ++ [ast] 91 | end 92 | end 93 | 94 | defp maybe_append_url_parameters_function_arguments(function_arguments, url_parameters, client_module_name) do 95 | case url_parameters do 96 | [] -> 97 | function_arguments 98 | 99 | url_parameters -> 100 | url_parameters = 101 | url_parameters 102 | |> Keyword.keys() 103 | |> Enum.map(&Ast.to_var(&1, client_module_name)) 104 | 105 | function_arguments ++ url_parameters 106 | end 107 | end 108 | 109 | # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity 110 | defp build_request_function_ast( 111 | client_module_name, 112 | func_name, 113 | method, 114 | request_path, 115 | function_arguments, 116 | request_body_arguments, 117 | url_parameters 118 | ) do 119 | opts = 120 | cond do 121 | url_parameters == [] and is_nil(request_body_arguments) -> [] 122 | is_nil(request_body_arguments) -> [params: url_parameters] 123 | url_parameters == [] -> [json: elem(request_body_arguments, 0)] 124 | true -> [json: elem(request_body_arguments, 0), params: url_parameters] 125 | end 126 | 127 | opts = [url: Ast.to_var("url", client_module_name)] ++ opts 128 | 129 | quote location: :keep do 130 | def unquote(Ast.sanitize_name(func_name))(unquote_splicing(function_arguments)) do 131 | url = unquote(request_path) 132 | 133 | unquote( 134 | case method do 135 | :get -> quote do: Req.get!(@req, unquote(opts)) 136 | :post -> quote do: Req.post!(@req, unquote(opts)) 137 | :put -> quote do: Req.put!(@req, unquote(opts)) 138 | :patch -> quote do: Req.patch!(@req, unquote(opts)) 139 | :delete -> quote do: Req.delete!(@req, unquote(opts)) 140 | end 141 | ) 142 | end 143 | end 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /lib/openapi_codegen/client/req/path.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenApiCodeGen.Client.Req.Path do 2 | @moduledoc """ 3 | Path generation operations for Req. 4 | """ 5 | 6 | alias OpenApiCodeGen.Ast 7 | 8 | @doc """ 9 | Generates an interpolated string to be used by the client. 10 | 11 | ## Params 12 | 13 | * `client_module_name` module name of the generated client 14 | * `path` to generate the request path with string interpolation 15 | """ 16 | def generate(client_module_name, path), do: Ast.generate_path_interpolation(client_module_name, path) 17 | end 18 | -------------------------------------------------------------------------------- /lib/openapi_codegen/client/request_body.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenApiCodeGen.Client.RequestBody do 2 | @moduledoc """ 3 | Generates request body AST from OpenAPI spec using the `requestBody` key. 4 | """ 5 | alias OpenApiCodeGen.Ast 6 | 7 | @doc """ 8 | Generates request body AST from OpenAPI spec using the `requestBody` key. 9 | 10 | The rules for generation are as follows: 11 | * When the request body is of type `application/json` and has a `$ref` key, it will generate a variable with the name of the component and assign it to the variable. 12 | * When the request body is of type `application/json` and has items, it will generate a variable with the name of the component plurarized and assign it to the variable. 13 | * Otherwise, it will be a variable named `body` and will be assigned to the variable. 14 | """ 15 | def generate(client_module_name, spec) do 16 | case spec do 17 | %{"requestBody" => %{"content" => %{"application/json" => %{"schema" => %{"$ref" => ref}}}}} -> 18 | ref_to_var_ast(client_module_name, ref, :single) 19 | 20 | %{"requestBody" => %{"content" => %{"application/json" => %{"schema" => %{"items" => %{"$ref" => ref}}}}}} -> 21 | ref_to_var_ast(client_module_name, ref, :array) 22 | 23 | %{"requestBody" => _} -> 24 | var = Ast.to_var(:body, client_module_name) 25 | {var, quote(do: unquote(var))} 26 | 27 | _ -> 28 | nil 29 | end 30 | end 31 | 32 | defp ref_to_var_ast(client_module_name, ref, type) do 33 | component_name = component_name_from_ref(ref, client_module_name) 34 | var = var_from_module(component_name, type) 35 | 36 | ast = 37 | case type do 38 | :single -> quote do: %unquote(component_name){} = unquote(var) 39 | _ -> quote do: unquote(var) 40 | end 41 | 42 | {var, ast} 43 | end 44 | 45 | defp component_name_from_ref(ref, client_module_name) do 46 | ref 47 | |> String.split("/") 48 | |> Enum.take(-1) 49 | |> hd() 50 | |> Ast.sanitize_name(:camelize) 51 | |> then(&String.to_atom("#{client_module_name}.#{&1}")) 52 | end 53 | 54 | defp var_from_module(client_module_name, type) do 55 | client_module_name 56 | |> Atom.to_string() 57 | |> String.split(".") 58 | |> Enum.take(-1) 59 | |> hd() 60 | |> then( 61 | &case type do 62 | :array -> "#{&1}s" 63 | _ -> &1 64 | end 65 | ) 66 | |> String.downcase() 67 | |> String.to_atom() 68 | |> Macro.var(client_module_name) 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/openapi_codegen/client/tesla/client.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenApiCodeGen.Client.Tesla do 2 | @moduledoc """ 3 | Tesla client code generation. 4 | """ 5 | 6 | alias OpenApiCodeGen.Ast 7 | alias OpenApiCodeGen.Client.QueryParam 8 | alias OpenApiCodeGen.Client.RequestBody 9 | alias OpenApiCodeGen.Client.Tesla.Path 10 | 11 | @path_elements_pattern ~r/{([^}]*)}/ 12 | 13 | @doc """ 14 | Generates client AST from OpenAPI spec using: 15 | 16 | ## Params 17 | 18 | * `servers` to generate Tesla middleware for the base URL 19 | * `operationId` for the name of the functions 20 | * `paths` keys to determine method for the function 21 | * `requestBody` to generate the request body argument 22 | * `path` to generate the request path with string interpolation 23 | * `parameters` to generate the URL parameters (if they are of type `query`) 24 | """ 25 | @spec generate(String.t(), map()) :: Macro.t() 26 | def generate(name, %{"paths" => paths, "servers" => servers}) do 27 | case servers do 28 | [%{"url" => server} | _] -> build_client_ast(name, paths, server) 29 | _ -> build_client_ast(name, paths, "") 30 | end 31 | end 32 | 33 | defp build_client_ast(name, paths, server) do 34 | client_module_name = String.to_atom("Elixir.#{name}") 35 | 36 | quote do 37 | defmodule unquote(client_module_name) do 38 | use Tesla 39 | 40 | plug(Tesla.Middleware.BaseUrl, unquote(server)) 41 | plug(Tesla.Middleware.JSON) 42 | 43 | unquote_splicing(generate_functions_ast(client_module_name, paths)) 44 | end 45 | end 46 | end 47 | 48 | defp generate_functions_ast(client_module_name, paths), do: Enum.map(paths, &generate_function(client_module_name, &1)) 49 | 50 | defp generate_function(client_module_name, spec) do 51 | case spec do 52 | {path, %{"get" => content}} -> generate_function(client_module_name, path, content, :get) 53 | {path, %{"post" => content}} -> generate_function(client_module_name, path, content, :post) 54 | {path, %{"put" => content}} -> generate_function(client_module_name, path, content, :put) 55 | {path, %{"delete" => content}} -> generate_function(client_module_name, path, content, :delete) 56 | {path, %{"patch" => content}} -> generate_function(client_module_name, path, content, :patch) 57 | end 58 | end 59 | 60 | defp generate_function(client_module_name, path, %{"operationId" => func_name} = schema, method) do 61 | request_body_arguments = RequestBody.generate(client_module_name, schema) 62 | url_parameters = QueryParam.generate(client_module_name, schema) 63 | request_path = Path.generate(client_module_name, path, url_parameters) 64 | 65 | function_arguments = generate_function_arguments(client_module_name, path, request_body_arguments, url_parameters) 66 | build_request_function_ast(func_name, method, request_path, function_arguments, request_body_arguments) 67 | end 68 | 69 | defp generate_function_arguments(client_module_name, path, request_body_arguments, url_parameters) do 70 | path_arguments = 71 | @path_elements_pattern 72 | |> Regex.scan(path) 73 | |> Enum.map(fn [_, arg] -> Ast.to_var(arg, client_module_name) end) 74 | 75 | path_arguments 76 | |> maybe_append_request_body_function_argument(request_body_arguments) 77 | |> maybe_append_url_parameters_function_arguments(url_parameters, client_module_name) 78 | end 79 | 80 | defp maybe_append_request_body_function_argument(function_arguments, request_body_arguments) do 81 | case request_body_arguments do 82 | nil -> function_arguments 83 | {_, ast} -> function_arguments ++ [ast] 84 | end 85 | end 86 | 87 | defp maybe_append_url_parameters_function_arguments(function_arguments, url_parameters, client_module_name) do 88 | case url_parameters do 89 | [] -> 90 | function_arguments 91 | 92 | url_parameters -> 93 | url_parameters = 94 | url_parameters 95 | |> Keyword.keys() 96 | |> Enum.map(&Ast.to_var(&1, client_module_name)) 97 | 98 | function_arguments ++ url_parameters 99 | end 100 | end 101 | 102 | # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity 103 | defp build_request_function_ast(func_name, method, request_path, function_arguments, request_body_arguments) do 104 | quote do 105 | # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity 106 | def unquote(Ast.sanitize_name(func_name))(unquote_splicing(function_arguments)) do 107 | url = unquote(request_path) 108 | 109 | unquote( 110 | cond do 111 | method == :get -> quote do: get!(url) 112 | method == :post and is_nil(request_body_arguments) -> quote do: post!(url, %{}) 113 | method == :post -> quote do: post!(url, unquote(elem(request_body_arguments, 0))) 114 | method == :put and is_nil(request_body_arguments) -> quote do: put!(url, %{}) 115 | method == :put -> quote do: put!(url, unquote(elem(request_body_arguments, 0))) 116 | method == :patch and is_nil(request_body_arguments) -> quote do: patch!(url, %{}) 117 | method == :patch -> quote do: patch!(url, unquote(elem(request_body_arguments, 0))) 118 | method == :delete and is_nil(request_body_arguments) -> quote do: delete!(url) 119 | method == :delete -> quote do: delete!(url, unquote(elem(request_body_arguments, 0))) 120 | end 121 | ) 122 | end 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /lib/openapi_codegen/client/tesla/path.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenApiCodeGen.Client.Tesla.Path do 2 | @moduledoc """ 3 | Path generation operations for Tesla. 4 | """ 5 | 6 | alias OpenApiCodeGen.Ast 7 | 8 | @doc """ 9 | Uses Tesla.build_url with an interpolated string and a set of parameters to generate the URLs to be used by the client 10 | 11 | ## Params 12 | 13 | * `client_module_name` module name of the generated client 14 | * `path` to generate the request path with string interpolation 15 | * `url_parameters` generated by `OpenApi.Codegen.Client.QueryParam` to generate the URL parameters 16 | """ 17 | def generate(client_module_name, path, []), do: Ast.generate_path_interpolation(client_module_name, path) 18 | 19 | def generate(client_module_name, path, url_parameters) do 20 | quote do 21 | Tesla.build_url(unquote(Ast.generate_path_interpolation(client_module_name, path)), unquote(url_parameters)) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/openapi_codegen/components.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenApiCodeGen.Components do 2 | @moduledoc """ 3 | Generates components AST from OpenAPI spec using the `schemas` key. 4 | """ 5 | alias OpenApiCodeGen.Ast 6 | 7 | @doc """ 8 | Generates components AST from OpenAPI spec using the `schemas` key. 9 | 10 | Returns the name of the component and the AST for said component. 11 | """ 12 | @spec generate(String.t(), map()) :: list({String.t(), Macro.t()}) 13 | def generate(client_module_name, %{"components" => %{"schemas" => schemas}}), 14 | do: schemas |> Enum.map(&generate_component(client_module_name, &1)) |> Enum.reject(&is_nil/1) 15 | 16 | defp generate_component(client_module_name, {key, %{"properties" => properties, "required" => required}}) do 17 | {key, build_component_ast(client_module_name, key, properties, required)} 18 | end 19 | 20 | defp generate_component(client_module_name, {key, %{"properties" => properties}}) do 21 | {key, build_component_ast(client_module_name, key, properties)} 22 | end 23 | 24 | defp generate_component(client_module_name, {key, %{"type" => _}}) do 25 | {key, build_component_ast(client_module_name, key, %{key => ""})} 26 | end 27 | 28 | defp generate_component(_, _), do: nil 29 | 30 | defp build_component_ast(client_module_name, key, properties, required \\ []) do 31 | component_module_name = 32 | key 33 | |> Ast.sanitize_name(:camelize) 34 | |> then(&String.to_atom("Elixir.#{client_module_name}.#{&1}")) 35 | 36 | quote do 37 | defmodule unquote(component_module_name) do 38 | @moduledoc unquote("Structure for #{key} component") 39 | @derive Jason.Encoder 40 | @enforce_keys unquote(Enum.map(required, &Ast.sanitize_name/1)) 41 | defstruct( 42 | unquote( 43 | properties 44 | |> Map.keys() 45 | |> Enum.map(&Ast.sanitize_name/1) 46 | ) 47 | ) 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule OpenApiCodeGen.MixProject do 2 | use Mix.Project 3 | 4 | @app :openapi_codegen 5 | @version "0.1.0" 6 | @description "OpenApiCodeGen is a Code Generation tool for Elixir" 7 | 8 | def project do 9 | [ 10 | app: @app, 11 | version: @version, 12 | description: @description, 13 | elixir: "~> 1.15", 14 | start_permanent: Mix.env() == :prod, 15 | consolidate_protocols: Mix.env() != :test, 16 | escript: escript(), 17 | package: package(), 18 | docs: docs(), 19 | deps: deps(), 20 | aliases: aliases(), 21 | dialyzer: [plt_add_apps: [:ex_unit]], 22 | preferred_cli_env: [ 23 | check: :test 24 | ] 25 | ] 26 | end 27 | 28 | def application do 29 | [extra_applications: [:logger]] 30 | end 31 | 32 | defp escript do 33 | [main_module: OpenApiCodeGen.CLI] 34 | end 35 | 36 | defp docs do 37 | [ 38 | main: "readme", 39 | extras: [{:"README.md", [title: "Overview"]}], 40 | source_ref: "v#{@version}" 41 | ] 42 | end 43 | 44 | defp package do 45 | [ 46 | licenses: ["MIT"], 47 | links: %{"GitHub" => "https://github.com/marmelasoft/openapi_codegen"} 48 | ] 49 | end 50 | 51 | defp deps do 52 | [ 53 | # http clients 54 | {:tesla, "~> 1.7"}, 55 | {:req, "~> 0.4.0"}, 56 | 57 | # parsers 58 | {:jason, "~> 1.4"}, 59 | {:yaml_elixir, "~> 2.9"}, 60 | 61 | # tools 62 | {:styler, "~> 0.8"}, 63 | {:credo, "~> 1.7"}, 64 | {:dialyxir, "~> 1.3", only: [:dev, :test], runtime: false}, 65 | {:ex_doc, "~> 0.30", only: :dev, runtime: false} 66 | ] 67 | end 68 | 69 | defp aliases do 70 | [ 71 | setup: ["deps.get"], 72 | "lint.credo": ["credo --strict --all"], 73 | "lint.dialyzer": ["dialyzer --format dialyxir"], 74 | lint: ["lint.dialyzer", "lint.credo"], 75 | check: [ 76 | "clean", 77 | "deps.unlock --check-unused", 78 | "compile --warnings-as-errors", 79 | "format --check-formatted", 80 | "deps.unlock --check-unused", 81 | "test --warnings-as-errors", 82 | "lint" 83 | ] 84 | ] 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 3 | "castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"}, 4 | "credo": {:hex, :credo, "1.7.5", "643213503b1c766ec0496d828c90c424471ea54da77c8a168c725686377b9545", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f799e9b5cd1891577d8c773d245668aa74a2fcd15eb277f51a0131690ebfb3fd"}, 5 | "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, 6 | "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, 7 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 8 | "ex_doc": {:hex, :ex_doc, "0.32.1", "21e40f939515373bcdc9cffe65f3b3543f05015ac6c3d01d991874129d173420", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "5142c9db521f106d61ff33250f779807ed2a88620e472ac95dc7d59c380113da"}, 9 | "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, 10 | "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, 11 | "hpax": {:hex, :hpax, "0.2.0", "5a58219adcb75977b2edce5eb22051de9362f08236220c9e859a47111c194ff5", [:mix], [], "hexpm", "bea06558cdae85bed075e6c036993d43cd54d447f76d8190a8db0dc5893fa2f1"}, 12 | "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, 13 | "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, 14 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, 15 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"}, 16 | "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, 17 | "mint": {:hex, :mint, "1.6.0", "88a4f91cd690508a04ff1c3e28952f322528934be541844d54e0ceb765f01d5e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "3c5ae85d90a5aca0a49c0d8b67360bbe407f3b54f1030a111047ff988e8fefaa"}, 18 | "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, 19 | "nimble_ownership": {:hex, :nimble_ownership, "0.3.1", "99d5244672fafdfac89bfad3d3ab8f0d367603ce1dc4855f86a1c75008bce56f", [:mix], [], "hexpm", "4bf510adedff0449a1d6e200e43e57a814794c8b5b6439071274d248d272a549"}, 20 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 21 | "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, 22 | "req": {:hex, :req, "0.4.14", "103de133a076a31044e5458e0f850d5681eef23dfabf3ea34af63212e3b902e2", [:mix], [{:aws_signature, "~> 0.3.2", [hex: :aws_signature, repo: "hexpm", optional: true]}, {:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:nimble_ownership, "~> 0.2.0 or ~> 0.3.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "2ddd3d33f9ab714ced8d3c15fd03db40c14dbf129003c4a3eb80fac2cc0b1b08"}, 23 | "styler": {:hex, :styler, "0.11.9", "2595393b94e660cd6e8b582876337cc50ff047d184ccbed42fdad2bfd5d78af5", [:mix], [], "hexpm", "8b7806ba1fdc94d0a75127c56875f91db89b75117fcc67572661010c13e1f259"}, 24 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, 25 | "tesla": {:hex, :tesla, "1.9.0", "8c22db6a826e56a087eeb8cdef56889731287f53feeb3f361dec5d4c8efb6f14", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "7c240c67e855f7e63e795bf16d6b3f5115a81d1f44b7fe4eadbf656bae0fef8a"}, 26 | "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, 27 | "yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"}, 28 | } 29 | -------------------------------------------------------------------------------- /test/integration_test.exs: -------------------------------------------------------------------------------- 1 | defmodule IntegrationTest do 2 | use ExUnit.Case, async: false 3 | 4 | @moduletag :integration 5 | 6 | describe "pet store spec integration test" do 7 | setup do 8 | Code.put_compiler_option(:ignore_module_conflict, true) 9 | 10 | on_exit(fn -> 11 | Code.put_compiler_option(:ignore_module_conflict, false) 12 | File.rm_rf!("tmp") 13 | end) 14 | end 15 | 16 | test "generated Tesla client is able to call service" do 17 | %{client: client, schemas: schemas} = 18 | OpenApiCodeGen.CLI.main([ 19 | "--tesla", 20 | "--output-path", 21 | "tmp/integration/tesla/petstore_client", 22 | "test/support/fixtures/openapi_petstore.json" 23 | ]) 24 | 25 | Enum.each(schemas ++ [client], &Code.compile_file/1) 26 | 27 | assert_functions(Tesla.Env) 28 | end 29 | 30 | test "generated Req client is able to call service" do 31 | %{client: client, schemas: schemas} = 32 | OpenApiCodeGen.CLI.main([ 33 | "--req", 34 | "--output-path", 35 | "tmp/integration/req/petstore_client", 36 | "test/support/fixtures/openapi_petstore.json" 37 | ]) 38 | 39 | Enum.each(schemas ++ [client], &Code.compile_file/1) 40 | 41 | assert_functions(Req.Response) 42 | end 43 | 44 | test "parsing yaml is the same as json" do 45 | result_json = 46 | %{client: _client, schemas: _schemas} = 47 | OpenApiCodeGen.CLI.main([ 48 | "--output-path", 49 | "tmp/integration/req/petstore_client", 50 | "test/support/fixtures/petstore/openapi_v31.json" 51 | ]) 52 | 53 | result_yaml = 54 | OpenApiCodeGen.CLI.main([ 55 | "--output-path", 56 | "tmp/integration/req/petstore_client", 57 | "test/support/fixtures/petstore/openapi_v31.yaml" 58 | ]) 59 | 60 | result_yml = 61 | OpenApiCodeGen.CLI.main([ 62 | "--output-path", 63 | "tmp/integration/req/petstore_client", 64 | "test/support/fixtures/petstore/openapi_v31.yml" 65 | ]) 66 | 67 | assert result_json == result_yaml 68 | assert result_json == result_yml 69 | end 70 | end 71 | 72 | # credo:disable-for-lines Credo.Check.Refactor.Apply 73 | defp assert_functions(expected_struct) do 74 | pet = struct(PetstoreClient.Pet, name: "test", id: 1) 75 | user = struct(PetstoreClient.User, username: "test", id: 1) 76 | order = struct(PetstoreClient.Order, id: 1) 77 | 78 | assert is_struct(apply(PetstoreClient, :find_pets_by_status, ["status"]), expected_struct) 79 | # FIXME: find pets by tags is returning 500 due to issues from the server 80 | assert is_struct(apply(PetstoreClient, :find_pets_by_tags, ["tag"]), expected_struct) 81 | # FIXME: get inventory is returning 500 due to issues from the server 82 | assert is_struct(apply(PetstoreClient, :get_inventory, []), expected_struct) 83 | assert is_struct(apply(PetstoreClient, :get_order_by_id, [1]), expected_struct) 84 | assert is_struct(apply(PetstoreClient, :get_user_by_name, ["username"]), expected_struct) 85 | assert is_struct(apply(PetstoreClient, :get_pet_by_id, [1]), expected_struct) 86 | 87 | assert is_struct(apply(PetstoreClient, :create_user, [user]), expected_struct) 88 | assert is_struct(apply(PetstoreClient, :add_pet, [pet]), expected_struct) 89 | assert is_struct(apply(PetstoreClient, :place_order, [order]), expected_struct) 90 | assert is_struct(apply(PetstoreClient, :create_users_with_list_input, [[user]]), expected_struct) 91 | 92 | # TODO: currently failing due to forced json decoding 93 | # assert is_struct(apply(PetstoreClient, :login_user, ["user", "password"]), expected_struct) 94 | # assert is_struct(apply(PetstoreClient, :logout_user, []), expected_struct) 95 | 96 | # TODO: currently fails because of forced json encoding instead of octet-stream 97 | # assert is_struct(apply(PetstoreClient, :upload_file, [1, "body", "additional_metadata"]), expected_struct) 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /test/openapi_codegen_test.exs: -------------------------------------------------------------------------------- 1 | defmodule OpenApiCodeGenTest do 2 | use ExUnit.Case 3 | 4 | doctest OpenApiCodeGen 5 | 6 | setup do 7 | content = 8 | "test/support/fixtures/openapi_petstore.json" 9 | |> File.read!() 10 | |> Jason.decode!() 11 | 12 | output_path = "tmp/lib/pet_store" 13 | 14 | on_exit(fn -> File.rm_rf!("tmp") end) 15 | %{content: content, output_path: output_path} 16 | end 17 | 18 | describe "generate/1" do 19 | test "generates components", %{content: content, output_path: output_path} do 20 | components = [ 21 | "Address", 22 | "ApiResponse", 23 | "Category", 24 | "Customer", 25 | "Order", 26 | "Pet", 27 | "Tag", 28 | "User" 29 | ] 30 | 31 | %{schemas: result} = OpenApiCodeGen.generate(output_path, content, :tesla) 32 | 33 | assert result == Enum.map(components, &Path.join(output_path <> "/components", "#{&1}.ex")) 34 | 35 | for component <- components do 36 | output_file = Path.join([output_path, "/components", "#{component}.ex"]) 37 | assert File.exists?(output_file) 38 | assert File.read!(output_file) == File.read!("test/support/fixtures/expected/tesla/components/#{component}.ex") 39 | end 40 | end 41 | 42 | test "generates tesla client", %{content: content, output_path: output_path} do 43 | %{client: output_file} = OpenApiCodeGen.generate(output_path, content, :tesla) 44 | assert File.exists?(output_file) 45 | assert File.read!(output_file) == File.read!("test/support/fixtures/expected/tesla/PetStore.ex") 46 | end 47 | 48 | test "generates req client", %{content: content, output_path: output_path} do 49 | %{client: output_file} = OpenApiCodeGen.generate(output_path, content, :req) 50 | assert File.exists?(output_file) 51 | assert File.read!(output_file) == File.read!("test/support/fixtures/expected/req/PetStore.ex") 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/support/fixtures/expected/req/PetStore.ex: -------------------------------------------------------------------------------- 1 | defmodule PetStore do 2 | @moduledoc false 3 | @req Req.new(base_url: "https://petstore3.swagger.io/api/v3") 4 | def add_pet(%PetStore.Pet{} = pet) do 5 | url = "/pet" 6 | Req.post!(@req, url: url, json: pet) 7 | end 8 | 9 | def find_pets_by_status(status) do 10 | url = "/pet/findByStatus" 11 | Req.get!(@req, url: url, params: [status: status]) 12 | end 13 | 14 | def find_pets_by_tags(tags) do 15 | url = "/pet/findByTags" 16 | Req.get!(@req, url: url, params: [tags: tags]) 17 | end 18 | 19 | def get_pet_by_id(pet_id) do 20 | url = "/pet/#{pet_id}" 21 | Req.get!(@req, url: url) 22 | end 23 | 24 | def upload_file(pet_id, body, additional_metadata) do 25 | url = "/pet/#{pet_id}/uploadImage" 26 | Req.post!(@req, url: url, json: body, params: [additional_metadata: additional_metadata]) 27 | end 28 | 29 | def get_inventory do 30 | url = "/store/inventory" 31 | Req.get!(@req, url: url) 32 | end 33 | 34 | def place_order(%PetStore.Order{} = order) do 35 | url = "/store/order" 36 | Req.post!(@req, url: url, json: order) 37 | end 38 | 39 | def get_order_by_id(order_id) do 40 | url = "/store/order/#{order_id}" 41 | Req.get!(@req, url: url) 42 | end 43 | 44 | def create_user(%PetStore.User{} = user) do 45 | url = "/user" 46 | Req.post!(@req, url: url, json: user) 47 | end 48 | 49 | def create_users_with_list_input(users) do 50 | url = "/user/createWithList" 51 | Req.post!(@req, url: url, json: users) 52 | end 53 | 54 | def login_user(username, password) do 55 | url = "/user/login" 56 | Req.get!(@req, url: url, params: [username: username, password: password]) 57 | end 58 | 59 | def logout_user do 60 | url = "/user/logout" 61 | Req.get!(@req, url: url) 62 | end 63 | 64 | def get_user_by_name(username) do 65 | url = "/user/#{username}" 66 | Req.get!(@req, url: url) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/support/fixtures/expected/req/components/Address.ex: -------------------------------------------------------------------------------- 1 | defmodule PetStore.Address do 2 | @moduledoc "Structure for Address component" 3 | @derive Jason.Encoder 4 | @enforce_keys [] 5 | defstruct [:city, :state, :street, :zip] 6 | end 7 | -------------------------------------------------------------------------------- /test/support/fixtures/expected/req/components/ApiResponse.ex: -------------------------------------------------------------------------------- 1 | defmodule PetStore.ApiResponse do 2 | @moduledoc "Structure for ApiResponse component" 3 | @derive Jason.Encoder 4 | @enforce_keys [] 5 | defstruct [:code, :message, :type] 6 | end 7 | -------------------------------------------------------------------------------- /test/support/fixtures/expected/req/components/Category.ex: -------------------------------------------------------------------------------- 1 | defmodule PetStore.Category do 2 | @moduledoc "Structure for Category component" 3 | @derive Jason.Encoder 4 | @enforce_keys [] 5 | defstruct [:id, :name] 6 | end 7 | -------------------------------------------------------------------------------- /test/support/fixtures/expected/req/components/Customer.ex: -------------------------------------------------------------------------------- 1 | defmodule PetStore.Customer do 2 | @moduledoc "Structure for Customer component" 3 | @derive Jason.Encoder 4 | @enforce_keys [] 5 | defstruct [:address, :id, :username] 6 | end 7 | -------------------------------------------------------------------------------- /test/support/fixtures/expected/req/components/Order.ex: -------------------------------------------------------------------------------- 1 | defmodule PetStore.Order do 2 | @moduledoc "Structure for Order component" 3 | @derive Jason.Encoder 4 | @enforce_keys [] 5 | defstruct [:complete, :id, :pet_id, :quantity, :ship_date, :status] 6 | end 7 | -------------------------------------------------------------------------------- /test/support/fixtures/expected/req/components/Pet.ex: -------------------------------------------------------------------------------- 1 | defmodule PetStore.Pet do 2 | @moduledoc "Structure for Pet component" 3 | @derive Jason.Encoder 4 | @enforce_keys [:name, :photo_urls] 5 | defstruct [:category, :id, :name, :photo_urls, :status, :tags] 6 | end 7 | -------------------------------------------------------------------------------- /test/support/fixtures/expected/req/components/Tag.ex: -------------------------------------------------------------------------------- 1 | defmodule PetStore.Tag do 2 | @moduledoc "Structure for Tag component" 3 | @derive Jason.Encoder 4 | @enforce_keys [] 5 | defstruct [:id, :name] 6 | end 7 | -------------------------------------------------------------------------------- /test/support/fixtures/expected/req/components/User.ex: -------------------------------------------------------------------------------- 1 | defmodule PetStore.User do 2 | @moduledoc "Structure for User component" 3 | @derive Jason.Encoder 4 | @enforce_keys [:id, :username] 5 | defstruct [:email, :first_name, :id, :last_name, :password, :phone, :user_status, :username] 6 | end 7 | -------------------------------------------------------------------------------- /test/support/fixtures/expected/tesla/PetStore.ex: -------------------------------------------------------------------------------- 1 | defmodule PetStore do 2 | @moduledoc false 3 | use Tesla 4 | 5 | plug(Tesla.Middleware.BaseUrl, "https://petstore3.swagger.io/api/v3") 6 | plug(Tesla.Middleware.JSON) 7 | 8 | def add_pet(%PetStore.Pet{} = pet) do 9 | url = "/pet" 10 | post!(url, pet) 11 | end 12 | 13 | def find_pets_by_status(status) do 14 | url = Tesla.build_url("/pet/findByStatus", status: status) 15 | get!(url) 16 | end 17 | 18 | def find_pets_by_tags(tags) do 19 | url = Tesla.build_url("/pet/findByTags", tags: tags) 20 | get!(url) 21 | end 22 | 23 | def get_pet_by_id(pet_id) do 24 | url = "/pet/#{pet_id}" 25 | get!(url) 26 | end 27 | 28 | def upload_file(pet_id, body, additional_metadata) do 29 | url = Tesla.build_url("/pet/#{pet_id}/uploadImage", additional_metadata: additional_metadata) 30 | post!(url, body) 31 | end 32 | 33 | def get_inventory do 34 | url = "/store/inventory" 35 | get!(url) 36 | end 37 | 38 | def place_order(%PetStore.Order{} = order) do 39 | url = "/store/order" 40 | post!(url, order) 41 | end 42 | 43 | def get_order_by_id(order_id) do 44 | url = "/store/order/#{order_id}" 45 | get!(url) 46 | end 47 | 48 | def create_user(%PetStore.User{} = user) do 49 | url = "/user" 50 | post!(url, user) 51 | end 52 | 53 | def create_users_with_list_input(users) do 54 | url = "/user/createWithList" 55 | post!(url, users) 56 | end 57 | 58 | def login_user(username, password) do 59 | url = Tesla.build_url("/user/login", username: username, password: password) 60 | get!(url) 61 | end 62 | 63 | def logout_user do 64 | url = "/user/logout" 65 | get!(url) 66 | end 67 | 68 | def get_user_by_name(username) do 69 | url = "/user/#{username}" 70 | get!(url) 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /test/support/fixtures/expected/tesla/components/Address.ex: -------------------------------------------------------------------------------- 1 | defmodule PetStore.Address do 2 | @moduledoc "Structure for Address component" 3 | @derive Jason.Encoder 4 | @enforce_keys [] 5 | defstruct [:city, :state, :street, :zip] 6 | end 7 | -------------------------------------------------------------------------------- /test/support/fixtures/expected/tesla/components/ApiResponse.ex: -------------------------------------------------------------------------------- 1 | defmodule PetStore.ApiResponse do 2 | @moduledoc "Structure for ApiResponse component" 3 | @derive Jason.Encoder 4 | @enforce_keys [] 5 | defstruct [:code, :message, :type] 6 | end 7 | -------------------------------------------------------------------------------- /test/support/fixtures/expected/tesla/components/Category.ex: -------------------------------------------------------------------------------- 1 | defmodule PetStore.Category do 2 | @moduledoc "Structure for Category component" 3 | @derive Jason.Encoder 4 | @enforce_keys [] 5 | defstruct [:id, :name] 6 | end 7 | -------------------------------------------------------------------------------- /test/support/fixtures/expected/tesla/components/Customer.ex: -------------------------------------------------------------------------------- 1 | defmodule PetStore.Customer do 2 | @moduledoc "Structure for Customer component" 3 | @derive Jason.Encoder 4 | @enforce_keys [] 5 | defstruct [:address, :id, :username] 6 | end 7 | -------------------------------------------------------------------------------- /test/support/fixtures/expected/tesla/components/Order.ex: -------------------------------------------------------------------------------- 1 | defmodule PetStore.Order do 2 | @moduledoc "Structure for Order component" 3 | @derive Jason.Encoder 4 | @enforce_keys [] 5 | defstruct [:complete, :id, :pet_id, :quantity, :ship_date, :status] 6 | end 7 | -------------------------------------------------------------------------------- /test/support/fixtures/expected/tesla/components/Pet.ex: -------------------------------------------------------------------------------- 1 | defmodule PetStore.Pet do 2 | @moduledoc "Structure for Pet component" 3 | @derive Jason.Encoder 4 | @enforce_keys [:name, :photo_urls] 5 | defstruct [:category, :id, :name, :photo_urls, :status, :tags] 6 | end 7 | -------------------------------------------------------------------------------- /test/support/fixtures/expected/tesla/components/Tag.ex: -------------------------------------------------------------------------------- 1 | defmodule PetStore.Tag do 2 | @moduledoc "Structure for Tag component" 3 | @derive Jason.Encoder 4 | @enforce_keys [] 5 | defstruct [:id, :name] 6 | end 7 | -------------------------------------------------------------------------------- /test/support/fixtures/expected/tesla/components/User.ex: -------------------------------------------------------------------------------- 1 | defmodule PetStore.User do 2 | @moduledoc "Structure for User component" 3 | @derive Jason.Encoder 4 | @enforce_keys [:id, :username] 5 | defstruct [:email, :first_name, :id, :last_name, :password, :phone, :user_status, :username] 6 | end 7 | -------------------------------------------------------------------------------- /test/support/fixtures/openapi_petstore.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.3", 3 | "info": { 4 | "title": "Swagger Petstore - OpenAPI 3.0", 5 | "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\n_If you're looking for the Swagger 2.0/OAS 2.0 version of Petstore, then click [here](https://editor.swagger.io/?url=https://petstore.swagger.io/v2/swagger.yaml). Alternatively, you can load via the `Edit > Load Petstore OAS 2.0` menu option!_\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)", 6 | "termsOfService": "http://swagger.io/terms/", 7 | "contact": { 8 | "email": "apiteam@swagger.io" 9 | }, 10 | "license": { 11 | "name": "Apache 2.0", 12 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 13 | }, 14 | "version": "1.0.11" 15 | }, 16 | "externalDocs": { 17 | "description": "Find out more about Swagger", 18 | "url": "http://swagger.io" 19 | }, 20 | "servers": [ 21 | { 22 | "url": "https://petstore3.swagger.io/api/v3" 23 | } 24 | ], 25 | "tags": [ 26 | { 27 | "name": "pet", 28 | "description": "Everything about your Pets", 29 | "externalDocs": { 30 | "description": "Find out more", 31 | "url": "http://swagger.io" 32 | } 33 | }, 34 | { 35 | "name": "store", 36 | "description": "Access to Petstore orders", 37 | "externalDocs": { 38 | "description": "Find out more about our store", 39 | "url": "http://swagger.io" 40 | } 41 | }, 42 | { 43 | "name": "user", 44 | "description": "Operations about user" 45 | } 46 | ], 47 | "paths": { 48 | "/pet": { 49 | "put": { 50 | "tags": [ 51 | "pet" 52 | ], 53 | "summary": "Update an existing pet", 54 | "description": "Update an existing pet by Id", 55 | "operationId": "updatePet", 56 | "requestBody": { 57 | "description": "Update an existent pet in the store", 58 | "content": { 59 | "application/json": { 60 | "schema": { 61 | "$ref": "#/components/schemas/Pet" 62 | } 63 | }, 64 | "application/xml": { 65 | "schema": { 66 | "$ref": "#/components/schemas/Pet" 67 | } 68 | }, 69 | "application/x-www-form-urlencoded": { 70 | "schema": { 71 | "$ref": "#/components/schemas/Pet" 72 | } 73 | } 74 | }, 75 | "required": true 76 | }, 77 | "responses": { 78 | "200": { 79 | "description": "Successful operation", 80 | "content": { 81 | "application/json": { 82 | "schema": { 83 | "$ref": "#/components/schemas/Pet" 84 | } 85 | }, 86 | "application/xml": { 87 | "schema": { 88 | "$ref": "#/components/schemas/Pet" 89 | } 90 | } 91 | } 92 | }, 93 | "400": { 94 | "description": "Invalid ID supplied" 95 | }, 96 | "404": { 97 | "description": "Pet not found" 98 | }, 99 | "405": { 100 | "description": "Validation exception" 101 | } 102 | }, 103 | "security": [ 104 | { 105 | "petstore_auth": [ 106 | "write:pets", 107 | "read:pets" 108 | ] 109 | } 110 | ] 111 | }, 112 | "post": { 113 | "tags": [ 114 | "pet" 115 | ], 116 | "summary": "Add a new pet to the store", 117 | "description": "Add a new pet to the store", 118 | "operationId": "addPet", 119 | "requestBody": { 120 | "description": "Create a new pet in the store", 121 | "content": { 122 | "application/json": { 123 | "schema": { 124 | "$ref": "#/components/schemas/Pet" 125 | } 126 | }, 127 | "application/xml": { 128 | "schema": { 129 | "$ref": "#/components/schemas/Pet" 130 | } 131 | }, 132 | "application/x-www-form-urlencoded": { 133 | "schema": { 134 | "$ref": "#/components/schemas/Pet" 135 | } 136 | } 137 | }, 138 | "required": true 139 | }, 140 | "responses": { 141 | "200": { 142 | "description": "Successful operation", 143 | "content": { 144 | "application/json": { 145 | "schema": { 146 | "$ref": "#/components/schemas/Pet" 147 | } 148 | }, 149 | "application/xml": { 150 | "schema": { 151 | "$ref": "#/components/schemas/Pet" 152 | } 153 | } 154 | } 155 | }, 156 | "405": { 157 | "description": "Invalid input" 158 | } 159 | }, 160 | "security": [ 161 | { 162 | "petstore_auth": [ 163 | "write:pets", 164 | "read:pets" 165 | ] 166 | } 167 | ] 168 | } 169 | }, 170 | "/pet/findByStatus": { 171 | "get": { 172 | "tags": [ 173 | "pet" 174 | ], 175 | "summary": "Finds Pets by status", 176 | "description": "Multiple status values can be provided with comma separated strings", 177 | "operationId": "findPetsByStatus", 178 | "parameters": [ 179 | { 180 | "name": "status", 181 | "in": "query", 182 | "description": "Status values that need to be considered for filter", 183 | "required": false, 184 | "explode": true, 185 | "schema": { 186 | "type": "string", 187 | "default": "available", 188 | "enum": [ 189 | "available", 190 | "pending", 191 | "sold" 192 | ] 193 | } 194 | } 195 | ], 196 | "responses": { 197 | "200": { 198 | "description": "successful operation", 199 | "content": { 200 | "application/json": { 201 | "schema": { 202 | "type": "array", 203 | "items": { 204 | "$ref": "#/components/schemas/Pet" 205 | } 206 | } 207 | }, 208 | "application/xml": { 209 | "schema": { 210 | "type": "array", 211 | "items": { 212 | "$ref": "#/components/schemas/Pet" 213 | } 214 | } 215 | } 216 | } 217 | }, 218 | "400": { 219 | "description": "Invalid status value" 220 | } 221 | }, 222 | "security": [ 223 | { 224 | "petstore_auth": [ 225 | "write:pets", 226 | "read:pets" 227 | ] 228 | } 229 | ] 230 | } 231 | }, 232 | "/pet/findByTags": { 233 | "get": { 234 | "tags": [ 235 | "pet" 236 | ], 237 | "summary": "Finds Pets by tags", 238 | "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", 239 | "operationId": "findPetsByTags", 240 | "parameters": [ 241 | { 242 | "name": "tags", 243 | "in": "query", 244 | "description": "Tags to filter by", 245 | "required": false, 246 | "explode": true, 247 | "schema": { 248 | "type": "array", 249 | "items": { 250 | "type": "string" 251 | } 252 | } 253 | } 254 | ], 255 | "responses": { 256 | "200": { 257 | "description": "successful operation", 258 | "content": { 259 | "application/json": { 260 | "schema": { 261 | "type": "array", 262 | "items": { 263 | "$ref": "#/components/schemas/Pet" 264 | } 265 | } 266 | }, 267 | "application/xml": { 268 | "schema": { 269 | "type": "array", 270 | "items": { 271 | "$ref": "#/components/schemas/Pet" 272 | } 273 | } 274 | } 275 | } 276 | }, 277 | "400": { 278 | "description": "Invalid tag value" 279 | } 280 | }, 281 | "security": [ 282 | { 283 | "petstore_auth": [ 284 | "write:pets", 285 | "read:pets" 286 | ] 287 | } 288 | ] 289 | } 290 | }, 291 | "/pet/{petId}": { 292 | "get": { 293 | "tags": [ 294 | "pet" 295 | ], 296 | "summary": "Find pet by ID", 297 | "description": "Returns a single pet", 298 | "operationId": "getPetById", 299 | "parameters": [ 300 | { 301 | "name": "petId", 302 | "in": "path", 303 | "description": "ID of pet to return", 304 | "required": true, 305 | "schema": { 306 | "type": "integer", 307 | "format": "int64" 308 | } 309 | } 310 | ], 311 | "responses": { 312 | "200": { 313 | "description": "successful operation", 314 | "content": { 315 | "application/json": { 316 | "schema": { 317 | "$ref": "#/components/schemas/Pet" 318 | } 319 | }, 320 | "application/xml": { 321 | "schema": { 322 | "$ref": "#/components/schemas/Pet" 323 | } 324 | } 325 | } 326 | }, 327 | "400": { 328 | "description": "Invalid ID supplied" 329 | }, 330 | "404": { 331 | "description": "Pet not found" 332 | } 333 | }, 334 | "security": [ 335 | { 336 | "api_key": [] 337 | }, 338 | { 339 | "petstore_auth": [ 340 | "write:pets", 341 | "read:pets" 342 | ] 343 | } 344 | ] 345 | }, 346 | "post": { 347 | "tags": [ 348 | "pet" 349 | ], 350 | "summary": "Updates a pet in the store with form data", 351 | "description": "", 352 | "operationId": "updatePetWithForm", 353 | "parameters": [ 354 | { 355 | "name": "petId", 356 | "in": "path", 357 | "description": "ID of pet that needs to be updated", 358 | "required": true, 359 | "schema": { 360 | "type": "integer", 361 | "format": "int64" 362 | } 363 | }, 364 | { 365 | "name": "name", 366 | "in": "query", 367 | "description": "Name of pet that needs to be updated", 368 | "schema": { 369 | "type": "string" 370 | } 371 | }, 372 | { 373 | "name": "status", 374 | "in": "query", 375 | "description": "Status of pet that needs to be updated", 376 | "schema": { 377 | "type": "string" 378 | } 379 | } 380 | ], 381 | "responses": { 382 | "405": { 383 | "description": "Invalid input" 384 | } 385 | }, 386 | "security": [ 387 | { 388 | "petstore_auth": [ 389 | "write:pets", 390 | "read:pets" 391 | ] 392 | } 393 | ] 394 | }, 395 | "delete": { 396 | "tags": [ 397 | "pet" 398 | ], 399 | "summary": "Deletes a pet", 400 | "description": "delete a pet", 401 | "operationId": "deletePet", 402 | "parameters": [ 403 | { 404 | "name": "api_key", 405 | "in": "header", 406 | "description": "", 407 | "required": false, 408 | "schema": { 409 | "type": "string" 410 | } 411 | }, 412 | { 413 | "name": "petId", 414 | "in": "path", 415 | "description": "Pet id to delete", 416 | "required": true, 417 | "schema": { 418 | "type": "integer", 419 | "format": "int64" 420 | } 421 | } 422 | ], 423 | "responses": { 424 | "400": { 425 | "description": "Invalid pet value" 426 | } 427 | }, 428 | "security": [ 429 | { 430 | "petstore_auth": [ 431 | "write:pets", 432 | "read:pets" 433 | ] 434 | } 435 | ] 436 | } 437 | }, 438 | "/pet/{petId}/uploadImage": { 439 | "post": { 440 | "tags": [ 441 | "pet" 442 | ], 443 | "summary": "uploads an image", 444 | "description": "", 445 | "operationId": "uploadFile", 446 | "parameters": [ 447 | { 448 | "name": "petId", 449 | "in": "path", 450 | "description": "ID of pet to update", 451 | "required": true, 452 | "schema": { 453 | "type": "integer", 454 | "format": "int64" 455 | } 456 | }, 457 | { 458 | "name": "additionalMetadata", 459 | "in": "query", 460 | "description": "Additional Metadata", 461 | "required": false, 462 | "schema": { 463 | "type": "string" 464 | } 465 | } 466 | ], 467 | "requestBody": { 468 | "content": { 469 | "application/octet-stream": { 470 | "schema": { 471 | "type": "string", 472 | "format": "binary" 473 | } 474 | } 475 | } 476 | }, 477 | "responses": { 478 | "200": { 479 | "description": "successful operation", 480 | "content": { 481 | "application/json": { 482 | "schema": { 483 | "$ref": "#/components/schemas/ApiResponse" 484 | } 485 | } 486 | } 487 | } 488 | }, 489 | "security": [ 490 | { 491 | "petstore_auth": [ 492 | "write:pets", 493 | "read:pets" 494 | ] 495 | } 496 | ] 497 | } 498 | }, 499 | "/store/inventory": { 500 | "get": { 501 | "tags": [ 502 | "store" 503 | ], 504 | "summary": "Returns pet inventories by status", 505 | "description": "Returns a map of status codes to quantities", 506 | "operationId": "getInventory", 507 | "responses": { 508 | "200": { 509 | "description": "successful operation", 510 | "content": { 511 | "application/json": { 512 | "schema": { 513 | "type": "object", 514 | "additionalProperties": { 515 | "type": "integer", 516 | "format": "int32" 517 | } 518 | } 519 | } 520 | } 521 | } 522 | }, 523 | "security": [ 524 | { 525 | "api_key": [] 526 | } 527 | ] 528 | } 529 | }, 530 | "/store/order": { 531 | "post": { 532 | "tags": [ 533 | "store" 534 | ], 535 | "summary": "Place an order for a pet", 536 | "description": "Place a new order in the store", 537 | "operationId": "placeOrder", 538 | "requestBody": { 539 | "content": { 540 | "application/json": { 541 | "schema": { 542 | "$ref": "#/components/schemas/Order" 543 | } 544 | }, 545 | "application/xml": { 546 | "schema": { 547 | "$ref": "#/components/schemas/Order" 548 | } 549 | }, 550 | "application/x-www-form-urlencoded": { 551 | "schema": { 552 | "$ref": "#/components/schemas/Order" 553 | } 554 | } 555 | } 556 | }, 557 | "responses": { 558 | "200": { 559 | "description": "successful operation", 560 | "content": { 561 | "application/json": { 562 | "schema": { 563 | "$ref": "#/components/schemas/Order" 564 | } 565 | } 566 | } 567 | }, 568 | "405": { 569 | "description": "Invalid input" 570 | } 571 | } 572 | } 573 | }, 574 | "/store/order/{orderId}": { 575 | "get": { 576 | "tags": [ 577 | "store" 578 | ], 579 | "summary": "Find purchase order by ID", 580 | "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.", 581 | "operationId": "getOrderById", 582 | "parameters": [ 583 | { 584 | "name": "orderId", 585 | "in": "path", 586 | "description": "ID of order that needs to be fetched", 587 | "required": true, 588 | "schema": { 589 | "type": "integer", 590 | "format": "int64" 591 | } 592 | } 593 | ], 594 | "responses": { 595 | "200": { 596 | "description": "successful operation", 597 | "content": { 598 | "application/json": { 599 | "schema": { 600 | "$ref": "#/components/schemas/Order" 601 | } 602 | }, 603 | "application/xml": { 604 | "schema": { 605 | "$ref": "#/components/schemas/Order" 606 | } 607 | } 608 | } 609 | }, 610 | "400": { 611 | "description": "Invalid ID supplied" 612 | }, 613 | "404": { 614 | "description": "Order not found" 615 | } 616 | } 617 | }, 618 | "delete": { 619 | "tags": [ 620 | "store" 621 | ], 622 | "summary": "Delete purchase order by ID", 623 | "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", 624 | "operationId": "deleteOrder", 625 | "parameters": [ 626 | { 627 | "name": "orderId", 628 | "in": "path", 629 | "description": "ID of the order that needs to be deleted", 630 | "required": true, 631 | "schema": { 632 | "type": "integer", 633 | "format": "int64" 634 | } 635 | } 636 | ], 637 | "responses": { 638 | "400": { 639 | "description": "Invalid ID supplied" 640 | }, 641 | "404": { 642 | "description": "Order not found" 643 | } 644 | } 645 | } 646 | }, 647 | "/user": { 648 | "post": { 649 | "tags": [ 650 | "user" 651 | ], 652 | "summary": "Create user", 653 | "description": "This can only be done by the logged in user.", 654 | "operationId": "createUser", 655 | "requestBody": { 656 | "description": "Created user object", 657 | "content": { 658 | "application/json": { 659 | "schema": { 660 | "$ref": "#/components/schemas/User" 661 | } 662 | }, 663 | "application/xml": { 664 | "schema": { 665 | "$ref": "#/components/schemas/User" 666 | } 667 | }, 668 | "application/x-www-form-urlencoded": { 669 | "schema": { 670 | "$ref": "#/components/schemas/User" 671 | } 672 | } 673 | } 674 | }, 675 | "responses": { 676 | "default": { 677 | "description": "successful operation", 678 | "content": { 679 | "application/json": { 680 | "schema": { 681 | "$ref": "#/components/schemas/User" 682 | } 683 | }, 684 | "application/xml": { 685 | "schema": { 686 | "$ref": "#/components/schemas/User" 687 | } 688 | } 689 | } 690 | } 691 | } 692 | } 693 | }, 694 | "/user/createWithList": { 695 | "post": { 696 | "tags": [ 697 | "user" 698 | ], 699 | "summary": "Creates list of users with given input array", 700 | "description": "Creates list of users with given input array", 701 | "operationId": "createUsersWithListInput", 702 | "requestBody": { 703 | "content": { 704 | "application/json": { 705 | "schema": { 706 | "type": "array", 707 | "items": { 708 | "$ref": "#/components/schemas/User" 709 | } 710 | } 711 | } 712 | } 713 | }, 714 | "responses": { 715 | "200": { 716 | "description": "Successful operation", 717 | "content": { 718 | "application/json": { 719 | "schema": { 720 | "$ref": "#/components/schemas/User" 721 | } 722 | }, 723 | "application/xml": { 724 | "schema": { 725 | "$ref": "#/components/schemas/User" 726 | } 727 | } 728 | } 729 | }, 730 | "default": { 731 | "description": "successful operation" 732 | } 733 | } 734 | } 735 | }, 736 | "/user/login": { 737 | "get": { 738 | "tags": [ 739 | "user" 740 | ], 741 | "summary": "Logs user into the system", 742 | "description": "", 743 | "operationId": "loginUser", 744 | "parameters": [ 745 | { 746 | "name": "username", 747 | "in": "query", 748 | "description": "The user name for login", 749 | "required": false, 750 | "schema": { 751 | "type": "string" 752 | } 753 | }, 754 | { 755 | "name": "password", 756 | "in": "query", 757 | "description": "The password for login in clear text", 758 | "required": false, 759 | "schema": { 760 | "type": "string" 761 | } 762 | } 763 | ], 764 | "responses": { 765 | "200": { 766 | "description": "successful operation", 767 | "headers": { 768 | "X-Rate-Limit": { 769 | "description": "calls per hour allowed by the user", 770 | "schema": { 771 | "type": "integer", 772 | "format": "int32" 773 | } 774 | }, 775 | "X-Expires-After": { 776 | "description": "date in UTC when token expires", 777 | "schema": { 778 | "type": "string", 779 | "format": "date-time" 780 | } 781 | } 782 | }, 783 | "content": { 784 | "application/xml": { 785 | "schema": { 786 | "type": "string" 787 | } 788 | }, 789 | "application/json": { 790 | "schema": { 791 | "type": "string" 792 | } 793 | } 794 | } 795 | }, 796 | "400": { 797 | "description": "Invalid username/password supplied" 798 | } 799 | } 800 | } 801 | }, 802 | "/user/logout": { 803 | "get": { 804 | "tags": [ 805 | "user" 806 | ], 807 | "summary": "Logs out current logged in user session", 808 | "description": "", 809 | "operationId": "logoutUser", 810 | "parameters": [], 811 | "responses": { 812 | "default": { 813 | "description": "successful operation" 814 | } 815 | } 816 | } 817 | }, 818 | "/user/{username}": { 819 | "get": { 820 | "tags": [ 821 | "user" 822 | ], 823 | "summary": "Get user by user name", 824 | "description": "", 825 | "operationId": "getUserByName", 826 | "parameters": [ 827 | { 828 | "name": "username", 829 | "in": "path", 830 | "description": "The name that needs to be fetched. Use user1 for testing. ", 831 | "required": true, 832 | "schema": { 833 | "type": "string" 834 | } 835 | } 836 | ], 837 | "responses": { 838 | "200": { 839 | "description": "successful operation", 840 | "content": { 841 | "application/json": { 842 | "schema": { 843 | "$ref": "#/components/schemas/User" 844 | } 845 | }, 846 | "application/xml": { 847 | "schema": { 848 | "$ref": "#/components/schemas/User" 849 | } 850 | } 851 | } 852 | }, 853 | "400": { 854 | "description": "Invalid username supplied" 855 | }, 856 | "404": { 857 | "description": "User not found" 858 | } 859 | } 860 | }, 861 | "put": { 862 | "tags": [ 863 | "user" 864 | ], 865 | "summary": "Update user", 866 | "description": "This can only be done by the logged in user.", 867 | "operationId": "updateUser", 868 | "parameters": [ 869 | { 870 | "name": "username", 871 | "in": "path", 872 | "description": "name that need to be deleted", 873 | "required": true, 874 | "schema": { 875 | "type": "string" 876 | } 877 | } 878 | ], 879 | "requestBody": { 880 | "description": "Update an existent user in the store", 881 | "content": { 882 | "application/json": { 883 | "schema": { 884 | "$ref": "#/components/schemas/User" 885 | } 886 | }, 887 | "application/xml": { 888 | "schema": { 889 | "$ref": "#/components/schemas/User" 890 | } 891 | }, 892 | "application/x-www-form-urlencoded": { 893 | "schema": { 894 | "$ref": "#/components/schemas/User" 895 | } 896 | } 897 | } 898 | }, 899 | "responses": { 900 | "default": { 901 | "description": "successful operation" 902 | } 903 | } 904 | }, 905 | "delete": { 906 | "tags": [ 907 | "user" 908 | ], 909 | "summary": "Delete user", 910 | "description": "This can only be done by the logged in user.", 911 | "operationId": "deleteUser", 912 | "parameters": [ 913 | { 914 | "name": "username", 915 | "in": "path", 916 | "description": "The name that needs to be deleted", 917 | "required": true, 918 | "schema": { 919 | "type": "string" 920 | } 921 | } 922 | ], 923 | "responses": { 924 | "400": { 925 | "description": "Invalid username supplied" 926 | }, 927 | "404": { 928 | "description": "User not found" 929 | } 930 | } 931 | } 932 | } 933 | }, 934 | "components": { 935 | "schemas": { 936 | "Order": { 937 | "type": "object", 938 | "properties": { 939 | "id": { 940 | "type": "integer", 941 | "format": "int64", 942 | "example": 10 943 | }, 944 | "petId": { 945 | "type": "integer", 946 | "format": "int64", 947 | "example": 198772 948 | }, 949 | "quantity": { 950 | "type": "integer", 951 | "format": "int32", 952 | "example": 7 953 | }, 954 | "shipDate": { 955 | "type": "string", 956 | "format": "date-time" 957 | }, 958 | "status": { 959 | "type": "string", 960 | "description": "Order Status", 961 | "example": "approved", 962 | "enum": [ 963 | "placed", 964 | "approved", 965 | "delivered" 966 | ] 967 | }, 968 | "complete": { 969 | "type": "boolean" 970 | } 971 | }, 972 | "xml": { 973 | "name": "order" 974 | } 975 | }, 976 | "Customer": { 977 | "type": "object", 978 | "properties": { 979 | "id": { 980 | "type": "integer", 981 | "format": "int64", 982 | "example": 100000 983 | }, 984 | "username": { 985 | "type": "string", 986 | "example": "fehguy" 987 | }, 988 | "address": { 989 | "type": "array", 990 | "xml": { 991 | "name": "addresses", 992 | "wrapped": true 993 | }, 994 | "items": { 995 | "$ref": "#/components/schemas/Address" 996 | } 997 | } 998 | }, 999 | "xml": { 1000 | "name": "customer" 1001 | } 1002 | }, 1003 | "Address": { 1004 | "type": "object", 1005 | "properties": { 1006 | "street": { 1007 | "type": "string", 1008 | "example": "437 Lytton" 1009 | }, 1010 | "city": { 1011 | "type": "string", 1012 | "example": "Palo Alto" 1013 | }, 1014 | "state": { 1015 | "type": "string", 1016 | "example": "CA" 1017 | }, 1018 | "zip": { 1019 | "type": "string", 1020 | "example": "94301" 1021 | } 1022 | }, 1023 | "xml": { 1024 | "name": "address" 1025 | } 1026 | }, 1027 | "Category": { 1028 | "type": "object", 1029 | "properties": { 1030 | "id": { 1031 | "type": "integer", 1032 | "format": "int64", 1033 | "example": 1 1034 | }, 1035 | "name": { 1036 | "type": "string", 1037 | "example": "Dogs" 1038 | } 1039 | }, 1040 | "xml": { 1041 | "name": "category" 1042 | } 1043 | }, 1044 | "User": { 1045 | "required": [ 1046 | "id", 1047 | "username" 1048 | ], 1049 | "type": "object", 1050 | "properties": { 1051 | "id": { 1052 | "type": "integer", 1053 | "format": "int64", 1054 | "example": 10 1055 | }, 1056 | "username": { 1057 | "type": "string", 1058 | "example": "theUser" 1059 | }, 1060 | "firstName": { 1061 | "type": "string", 1062 | "example": "John" 1063 | }, 1064 | "lastName": { 1065 | "type": "string", 1066 | "example": "James" 1067 | }, 1068 | "email": { 1069 | "type": "string", 1070 | "example": "john@email.com" 1071 | }, 1072 | "password": { 1073 | "type": "string", 1074 | "example": "12345" 1075 | }, 1076 | "phone": { 1077 | "type": "string", 1078 | "example": "12345" 1079 | }, 1080 | "userStatus": { 1081 | "type": "integer", 1082 | "description": "User Status", 1083 | "format": "int32", 1084 | "example": 1 1085 | } 1086 | }, 1087 | "xml": { 1088 | "name": "user" 1089 | } 1090 | }, 1091 | "Tag": { 1092 | "type": "object", 1093 | "properties": { 1094 | "id": { 1095 | "type": "integer", 1096 | "format": "int64" 1097 | }, 1098 | "name": { 1099 | "type": "string" 1100 | } 1101 | }, 1102 | "xml": { 1103 | "name": "tag" 1104 | } 1105 | }, 1106 | "Pet": { 1107 | "required": [ 1108 | "name", 1109 | "photoUrls" 1110 | ], 1111 | "type": "object", 1112 | "properties": { 1113 | "id": { 1114 | "type": "integer", 1115 | "format": "int64", 1116 | "example": 10 1117 | }, 1118 | "name": { 1119 | "type": "string", 1120 | "example": "doggie" 1121 | }, 1122 | "category": { 1123 | "$ref": "#/components/schemas/Category" 1124 | }, 1125 | "photoUrls": { 1126 | "type": "array", 1127 | "xml": { 1128 | "wrapped": true 1129 | }, 1130 | "items": { 1131 | "type": "string", 1132 | "xml": { 1133 | "name": "photoUrl" 1134 | } 1135 | } 1136 | }, 1137 | "tags": { 1138 | "type": "array", 1139 | "xml": { 1140 | "wrapped": true 1141 | }, 1142 | "items": { 1143 | "$ref": "#/components/schemas/Tag" 1144 | } 1145 | }, 1146 | "status": { 1147 | "type": "string", 1148 | "description": "pet status in the store", 1149 | "enum": [ 1150 | "available", 1151 | "pending", 1152 | "sold" 1153 | ] 1154 | } 1155 | }, 1156 | "xml": { 1157 | "name": "pet" 1158 | } 1159 | }, 1160 | "ApiResponse": { 1161 | "type": "object", 1162 | "properties": { 1163 | "code": { 1164 | "type": "integer", 1165 | "format": "int32" 1166 | }, 1167 | "type": { 1168 | "type": "string" 1169 | }, 1170 | "message": { 1171 | "type": "string" 1172 | } 1173 | }, 1174 | "xml": { 1175 | "name": "##default" 1176 | } 1177 | } 1178 | }, 1179 | "requestBodies": { 1180 | "Pet": { 1181 | "description": "Pet object that needs to be added to the store", 1182 | "content": { 1183 | "application/json": { 1184 | "schema": { 1185 | "$ref": "#/components/schemas/Pet" 1186 | } 1187 | }, 1188 | "application/xml": { 1189 | "schema": { 1190 | "$ref": "#/components/schemas/Pet" 1191 | } 1192 | } 1193 | } 1194 | }, 1195 | "UserArray": { 1196 | "description": "List of user object", 1197 | "content": { 1198 | "application/json": { 1199 | "schema": { 1200 | "type": "array", 1201 | "items": { 1202 | "$ref": "#/components/schemas/User" 1203 | } 1204 | } 1205 | } 1206 | } 1207 | } 1208 | }, 1209 | "securitySchemes": { 1210 | "petstore_auth": { 1211 | "type": "oauth2", 1212 | "flows": { 1213 | "implicit": { 1214 | "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", 1215 | "scopes": { 1216 | "write:pets": "modify pets in your account", 1217 | "read:pets": "read your pets" 1218 | } 1219 | } 1220 | } 1221 | }, 1222 | "api_key": { 1223 | "type": "apiKey", 1224 | "name": "api_key", 1225 | "in": "header" 1226 | } 1227 | } 1228 | } 1229 | } -------------------------------------------------------------------------------- /test/support/fixtures/petstore/openapi_v31.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.1.0", 3 | "info": { 4 | "title": "Swagger Petstore - OpenAPI 3.1", 5 | "description": "This is a sample Pet Store Server based on the OpenAPI 3.1 specification.\nYou can find out more about\nSwagger at [http://swagger.io](http://swagger.io).", 6 | "termsOfService": "http://swagger.io/terms/", 7 | "contact": { 8 | "email": "apiteam@swagger.io" 9 | }, 10 | "license": { 11 | "name": "Apache 2.0 AND (MIT OR GPL-2.0-only)", 12 | "identifier": "Apache-2.0 AND (MIT OR GPL-2.0-only)" 13 | }, 14 | "version": "1.0.2", 15 | "summary": "Pet Store 3.1", 16 | "x-namespace": "swagger" 17 | }, 18 | "externalDocs": { 19 | "description": "Find out more about Swagger", 20 | "url": "http://swagger.io" 21 | }, 22 | "servers": [ 23 | { 24 | "url": "/api/v31" 25 | } 26 | ], 27 | "tags": [ 28 | { 29 | "name": "pet", 30 | "description": "Everything about your Pets", 31 | "externalDocs": { 32 | "description": "Find out more", 33 | "url": "http://swagger.io" 34 | } 35 | }, 36 | { 37 | "name": "store", 38 | "description": "Access to Petstore orders", 39 | "externalDocs": { 40 | "description": "Find out more about our store", 41 | "url": "http://swagger.io" 42 | } 43 | }, 44 | { 45 | "name": "user", 46 | "description": "Operations about user" 47 | } 48 | ], 49 | "paths": { 50 | "/pet": { 51 | "put": { 52 | "tags": ["pet"], 53 | "summary": "Update an existing pet", 54 | "description": "Update an existing pet by Id", 55 | "operationId": "updatePet", 56 | "requestBody": { 57 | "description": "Pet object that needs to be updated in the store", 58 | "content": { 59 | "application/json": { 60 | "schema": { 61 | "$ref": "#/components/schemas/Pet", 62 | "description": "A Pet in JSON Format", 63 | "required": ["id"], 64 | "writeOnly": true 65 | } 66 | }, 67 | "application/xml": { 68 | "schema": { 69 | "$ref": "#/components/schemas/Pet", 70 | "description": "A Pet in XML Format", 71 | "required": ["id"], 72 | "writeOnly": true 73 | } 74 | } 75 | }, 76 | "required": true 77 | }, 78 | "responses": { 79 | "200": { 80 | "description": "Successful operation", 81 | "content": { 82 | "application/xml": { 83 | "schema": { 84 | "$ref": "#/components/schemas/Pet", 85 | "description": "A Pet in XML Format", 86 | "readOnly": true 87 | } 88 | }, 89 | "application/json": { 90 | "schema": { 91 | "$ref": "#/components/schemas/Pet", 92 | "description": "A Pet in JSON Format", 93 | "readOnly": true 94 | } 95 | } 96 | } 97 | }, 98 | "400": { 99 | "description": "Invalid ID supplied" 100 | }, 101 | "404": { 102 | "description": "Pet not found" 103 | }, 104 | "405": { 105 | "description": "Validation exception" 106 | } 107 | }, 108 | "security": [ 109 | { 110 | "petstore_auth": ["write:pets", "read:pets"] 111 | } 112 | ] 113 | }, 114 | "post": { 115 | "tags": ["pet"], 116 | "summary": "Add a new pet to the store", 117 | "description": "Add a new pet to the store", 118 | "operationId": "addPet", 119 | "requestBody": { 120 | "description": "Create a new pet in the store", 121 | "content": { 122 | "application/json": { 123 | "schema": { 124 | "$ref": "#/components/schemas/Pet", 125 | "description": "A Pet in JSON Format", 126 | "required": ["id"], 127 | "writeOnly": true 128 | } 129 | }, 130 | "application/xml": { 131 | "schema": { 132 | "$ref": "#/components/schemas/Pet", 133 | "description": "A Pet in XML Format", 134 | "required": ["id"], 135 | "writeOnly": true 136 | } 137 | } 138 | }, 139 | "required": true 140 | }, 141 | "responses": { 142 | "200": { 143 | "description": "Successful operation", 144 | "content": { 145 | "application/xml": { 146 | "schema": { 147 | "$ref": "#/components/schemas/Pet", 148 | "description": "A Pet in XML Format", 149 | "readOnly": true 150 | } 151 | }, 152 | "application/json": { 153 | "schema": { 154 | "$ref": "#/components/schemas/Pet", 155 | "description": "A Pet in JSON Format", 156 | "readOnly": true 157 | } 158 | } 159 | } 160 | }, 161 | "405": { 162 | "description": "Invalid input" 163 | } 164 | }, 165 | "security": [ 166 | { 167 | "petstore_auth": ["write:pets", "read:pets"] 168 | } 169 | ] 170 | } 171 | }, 172 | "/pet/{petId}": { 173 | "get": { 174 | "tags": ["pets"], 175 | "summary": "Find pet by ID", 176 | "description": "Returns a pet when 0 < ID <= 10. ID > 10 or nonintegers will simulate API error conditions", 177 | "operationId": "getPetById", 178 | "parameters": [ 179 | { 180 | "name": "petId", 181 | "in": "path", 182 | "description": "ID of pet that needs to be fetched", 183 | "required": true, 184 | "schema": { 185 | "type": "integer", 186 | "format": "int64", 187 | "description": "param ID of pet that needs to be fetched", 188 | "exclusiveMaximum": 10, 189 | "exclusiveMinimum": 1 190 | } 191 | } 192 | ], 193 | "responses": { 194 | "default": { 195 | "description": "The pet", 196 | "content": { 197 | "application/json": { 198 | "schema": { 199 | "$ref": "#/components/schemas/Pet", 200 | "description": "A Pet in JSON format" 201 | } 202 | }, 203 | "application/xml": { 204 | "schema": { 205 | "$ref": "#/components/schemas/Pet", 206 | "description": "A Pet in XML format" 207 | } 208 | } 209 | } 210 | }, 211 | "400": { 212 | "description": "Invalid ID supplied" 213 | }, 214 | "404": { 215 | "description": "Pet not found" 216 | } 217 | }, 218 | "security": [ 219 | { 220 | "petstore_auth": ["write:pets", "read:pets"] 221 | }, 222 | { 223 | "api_key": [] 224 | } 225 | ] 226 | } 227 | } 228 | }, 229 | "components": { 230 | "schemas": { 231 | "Category": { 232 | "$id": "/components/schemas/category", 233 | "description": "Category", 234 | "properties": { 235 | "id": { 236 | "type": "integer", 237 | "format": "int64", 238 | "example": 1 239 | }, 240 | "name": { 241 | "type": "string", 242 | "example": "Dogs" 243 | } 244 | }, 245 | "xml": { 246 | "name": "Category" 247 | } 248 | }, 249 | "Pet": { 250 | "$schema": "https://json-schema.org/draft/2020-12/schema", 251 | "description": "Pet", 252 | "properties": { 253 | "id": { 254 | "type": "integer", 255 | "format": "int64", 256 | "example": 10 257 | }, 258 | "category": { 259 | "$ref": "#/components/schemas/Category", 260 | "description": "Pet Category" 261 | }, 262 | "name": { 263 | "type": "string", 264 | "example": "doggie" 265 | }, 266 | "photoUrls": { 267 | "type": "array", 268 | "items": { 269 | "type": "string", 270 | "xml": { 271 | "name": "photoUrl" 272 | } 273 | }, 274 | "xml": { 275 | "wrapped": true 276 | } 277 | }, 278 | "tags": { 279 | "type": "array", 280 | "items": { 281 | "$ref": "#/components/schemas/Tag" 282 | }, 283 | "xml": { 284 | "wrapped": true 285 | } 286 | }, 287 | "status": { 288 | "type": "string", 289 | "description": "pet status in the store", 290 | "enum": ["available", "pending", "sold"] 291 | }, 292 | "availableInstances": { 293 | "type": "integer", 294 | "format": "int32", 295 | "example": 7, 296 | "exclusiveMaximum": 10, 297 | "exclusiveMinimum": 1, 298 | "swagger-extension": true 299 | }, 300 | "petDetailsId": { 301 | "type": "integer", 302 | "format": "int64", 303 | "$ref": "/components/schemas/petdetails#pet_details_id" 304 | }, 305 | "petDetails": { 306 | "$ref": "/components/schemas/petdetails" 307 | } 308 | }, 309 | "required": ["name", "photoUrls"], 310 | "xml": { 311 | "name": "Pet" 312 | } 313 | }, 314 | "PetDetails": { 315 | "$id": "/components/schemas/petdetails", 316 | "$schema": "https://json-schema.org/draft/2020-12/schema", 317 | "$vocabulary": "https://spec.openapis.org/oas/3.1/schema-base", 318 | "properties": { 319 | "id": { 320 | "type": "integer", 321 | "format": "int64", 322 | "$anchor": "pet_details_id", 323 | "example": 10 324 | }, 325 | "category": { 326 | "$ref": "/components/schemas/category", 327 | "description": "PetDetails Category" 328 | }, 329 | "tag": { 330 | "$ref": "/components/schemas/tag" 331 | } 332 | }, 333 | "xml": { 334 | "name": "PetDetails" 335 | } 336 | }, 337 | "Tag": { 338 | "$id": "/components/schemas/tag", 339 | "properties": { 340 | "id": { 341 | "type": "integer", 342 | "format": "int64" 343 | }, 344 | "name": { 345 | "type": "string" 346 | } 347 | }, 348 | "xml": { 349 | "name": "Tag" 350 | } 351 | } 352 | }, 353 | "securitySchemes": { 354 | "petstore_auth": { 355 | "type": "oauth2", 356 | "flows": { 357 | "implicit": { 358 | "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", 359 | "scopes": { 360 | "write:pets": "modify pets in your account", 361 | "read:pets": "read your pets" 362 | } 363 | } 364 | } 365 | }, 366 | "mutual_tls": { 367 | "type": "mutualTLS" 368 | }, 369 | "api_key": { 370 | "type": "apiKey", 371 | "name": "api_key", 372 | "in": "header" 373 | } 374 | } 375 | }, 376 | "webhooks": { 377 | "newPet": { 378 | "post": { 379 | "requestBody": { 380 | "description": "Information about a new pet in the system", 381 | "content": { 382 | "application/json": { 383 | "schema": { 384 | "$ref": "#/components/schemas/Pet", 385 | "description": "Webhook Pet" 386 | } 387 | } 388 | } 389 | }, 390 | "responses": { 391 | "200": { 392 | "description": "Return a 200 status to indicate that the data was received successfully" 393 | } 394 | } 395 | } 396 | } 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /test/support/fixtures/petstore/openapi_v31.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: Swagger Petstore - OpenAPI 3.1 4 | description: |- 5 | This is a sample Pet Store Server based on the OpenAPI 3.1 specification. 6 | You can find out more about 7 | Swagger at [http://swagger.io](http://swagger.io). 8 | termsOfService: http://swagger.io/terms/ 9 | contact: 10 | email: apiteam@swagger.io 11 | license: 12 | name: Apache 2.0 AND (MIT OR GPL-2.0-only) 13 | identifier: Apache-2.0 AND (MIT OR GPL-2.0-only) 14 | version: 1.0.2 15 | summary: Pet Store 3.1 16 | x-namespace: swagger 17 | externalDocs: 18 | description: Find out more about Swagger 19 | url: http://swagger.io 20 | servers: 21 | - url: /api/v31 22 | tags: 23 | - name: pet 24 | description: Everything about your Pets 25 | externalDocs: 26 | description: Find out more 27 | url: http://swagger.io 28 | - name: store 29 | description: Access to Petstore orders 30 | externalDocs: 31 | description: Find out more about our store 32 | url: http://swagger.io 33 | - name: user 34 | description: Operations about user 35 | paths: 36 | /pet: 37 | put: 38 | tags: 39 | - pet 40 | summary: Update an existing pet 41 | description: Update an existing pet by Id 42 | operationId: updatePet 43 | requestBody: 44 | description: Pet object that needs to be updated in the store 45 | content: 46 | application/json: 47 | schema: 48 | $ref: "#/components/schemas/Pet" 49 | description: A Pet in JSON Format 50 | required: 51 | - id 52 | writeOnly: true 53 | application/xml: 54 | schema: 55 | $ref: "#/components/schemas/Pet" 56 | description: A Pet in XML Format 57 | required: 58 | - id 59 | writeOnly: true 60 | required: true 61 | responses: 62 | "200": 63 | description: Successful operation 64 | content: 65 | application/xml: 66 | schema: 67 | $ref: "#/components/schemas/Pet" 68 | description: A Pet in XML Format 69 | readOnly: true 70 | application/json: 71 | schema: 72 | $ref: "#/components/schemas/Pet" 73 | description: A Pet in JSON Format 74 | readOnly: true 75 | "400": 76 | description: Invalid ID supplied 77 | "404": 78 | description: Pet not found 79 | "405": 80 | description: Validation exception 81 | security: 82 | - petstore_auth: 83 | - write:pets 84 | - read:pets 85 | post: 86 | tags: 87 | - pet 88 | summary: Add a new pet to the store 89 | description: Add a new pet to the store 90 | operationId: addPet 91 | requestBody: 92 | description: Create a new pet in the store 93 | content: 94 | application/json: 95 | schema: 96 | $ref: "#/components/schemas/Pet" 97 | description: A Pet in JSON Format 98 | required: 99 | - id 100 | writeOnly: true 101 | application/xml: 102 | schema: 103 | $ref: "#/components/schemas/Pet" 104 | description: A Pet in XML Format 105 | required: 106 | - id 107 | writeOnly: true 108 | required: true 109 | responses: 110 | "200": 111 | description: Successful operation 112 | content: 113 | application/xml: 114 | schema: 115 | $ref: "#/components/schemas/Pet" 116 | description: A Pet in XML Format 117 | readOnly: true 118 | application/json: 119 | schema: 120 | $ref: "#/components/schemas/Pet" 121 | description: A Pet in JSON Format 122 | readOnly: true 123 | "405": 124 | description: Invalid input 125 | security: 126 | - petstore_auth: 127 | - write:pets 128 | - read:pets 129 | /pet/{petId}: 130 | get: 131 | tags: 132 | - pets 133 | summary: Find pet by ID 134 | description: 135 | Returns a pet when 0 < ID <= 10. ID > 10 or nonintegers will simulate 136 | API error conditions 137 | operationId: getPetById 138 | parameters: 139 | - name: petId 140 | in: path 141 | description: ID of pet that needs to be fetched 142 | required: true 143 | schema: 144 | type: integer 145 | format: int64 146 | description: param ID of pet that needs to be fetched 147 | exclusiveMaximum: 10 148 | exclusiveMinimum: 1 149 | responses: 150 | default: 151 | description: The pet 152 | content: 153 | application/json: 154 | schema: 155 | $ref: "#/components/schemas/Pet" 156 | description: A Pet in JSON format 157 | application/xml: 158 | schema: 159 | $ref: "#/components/schemas/Pet" 160 | description: A Pet in XML format 161 | "400": 162 | description: Invalid ID supplied 163 | "404": 164 | description: Pet not found 165 | security: 166 | - petstore_auth: 167 | - write:pets 168 | - read:pets 169 | - api_key: [] 170 | components: 171 | schemas: 172 | Category: 173 | $id: /components/schemas/category 174 | description: Category 175 | properties: 176 | id: 177 | type: integer 178 | format: int64 179 | example: 1 180 | name: 181 | type: string 182 | example: Dogs 183 | xml: 184 | name: Category 185 | Pet: 186 | $schema: https://json-schema.org/draft/2020-12/schema 187 | description: Pet 188 | properties: 189 | id: 190 | type: integer 191 | format: int64 192 | example: 10 193 | category: 194 | $ref: "#/components/schemas/Category" 195 | description: Pet Category 196 | name: 197 | type: string 198 | example: doggie 199 | photoUrls: 200 | type: array 201 | items: 202 | type: string 203 | xml: 204 | name: photoUrl 205 | xml: 206 | wrapped: true 207 | tags: 208 | type: array 209 | items: 210 | $ref: "#/components/schemas/Tag" 211 | xml: 212 | wrapped: true 213 | status: 214 | type: string 215 | description: pet status in the store 216 | enum: 217 | - available 218 | - pending 219 | - sold 220 | availableInstances: 221 | type: integer 222 | format: int32 223 | example: 7 224 | exclusiveMaximum: 10 225 | exclusiveMinimum: 1 226 | swagger-extension: true 227 | petDetailsId: 228 | type: integer 229 | format: int64 230 | $ref: /components/schemas/petdetails#pet_details_id 231 | petDetails: 232 | $ref: /components/schemas/petdetails 233 | required: 234 | - name 235 | - photoUrls 236 | xml: 237 | name: Pet 238 | PetDetails: 239 | $id: /components/schemas/petdetails 240 | $schema: https://json-schema.org/draft/2020-12/schema 241 | $vocabulary: https://spec.openapis.org/oas/3.1/schema-base 242 | properties: 243 | id: 244 | type: integer 245 | format: int64 246 | $anchor: pet_details_id 247 | example: 10 248 | category: 249 | $ref: /components/schemas/category 250 | description: PetDetails Category 251 | tag: 252 | $ref: /components/schemas/tag 253 | xml: 254 | name: PetDetails 255 | Tag: 256 | $id: /components/schemas/tag 257 | properties: 258 | id: 259 | type: integer 260 | format: int64 261 | name: 262 | type: string 263 | xml: 264 | name: Tag 265 | securitySchemes: 266 | petstore_auth: 267 | type: oauth2 268 | flows: 269 | implicit: 270 | authorizationUrl: https://petstore3.swagger.io/oauth/authorize 271 | scopes: 272 | write:pets: modify pets in your account 273 | read:pets: read your pets 274 | mutual_tls: 275 | type: mutualTLS 276 | api_key: 277 | type: apiKey 278 | name: api_key 279 | in: header 280 | webhooks: 281 | newPet: 282 | post: 283 | requestBody: 284 | description: Information about a new pet in the system 285 | content: 286 | application/json: 287 | schema: 288 | $ref: "#/components/schemas/Pet" 289 | description: Webhook Pet 290 | responses: 291 | "200": 292 | description: 293 | Return a 200 status to indicate that the data was received 294 | successfully 295 | -------------------------------------------------------------------------------- /test/support/fixtures/petstore/openapi_v31.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: Swagger Petstore - OpenAPI 3.1 4 | description: |- 5 | This is a sample Pet Store Server based on the OpenAPI 3.1 specification. 6 | You can find out more about 7 | Swagger at [http://swagger.io](http://swagger.io). 8 | termsOfService: http://swagger.io/terms/ 9 | contact: 10 | email: apiteam@swagger.io 11 | license: 12 | name: Apache 2.0 AND (MIT OR GPL-2.0-only) 13 | identifier: Apache-2.0 AND (MIT OR GPL-2.0-only) 14 | version: 1.0.2 15 | summary: Pet Store 3.1 16 | x-namespace: swagger 17 | externalDocs: 18 | description: Find out more about Swagger 19 | url: http://swagger.io 20 | servers: 21 | - url: /api/v31 22 | tags: 23 | - name: pet 24 | description: Everything about your Pets 25 | externalDocs: 26 | description: Find out more 27 | url: http://swagger.io 28 | - name: store 29 | description: Access to Petstore orders 30 | externalDocs: 31 | description: Find out more about our store 32 | url: http://swagger.io 33 | - name: user 34 | description: Operations about user 35 | paths: 36 | /pet: 37 | put: 38 | tags: 39 | - pet 40 | summary: Update an existing pet 41 | description: Update an existing pet by Id 42 | operationId: updatePet 43 | requestBody: 44 | description: Pet object that needs to be updated in the store 45 | content: 46 | application/json: 47 | schema: 48 | $ref: "#/components/schemas/Pet" 49 | description: A Pet in JSON Format 50 | required: 51 | - id 52 | writeOnly: true 53 | application/xml: 54 | schema: 55 | $ref: "#/components/schemas/Pet" 56 | description: A Pet in XML Format 57 | required: 58 | - id 59 | writeOnly: true 60 | required: true 61 | responses: 62 | "200": 63 | description: Successful operation 64 | content: 65 | application/xml: 66 | schema: 67 | $ref: "#/components/schemas/Pet" 68 | description: A Pet in XML Format 69 | readOnly: true 70 | application/json: 71 | schema: 72 | $ref: "#/components/schemas/Pet" 73 | description: A Pet in JSON Format 74 | readOnly: true 75 | "400": 76 | description: Invalid ID supplied 77 | "404": 78 | description: Pet not found 79 | "405": 80 | description: Validation exception 81 | security: 82 | - petstore_auth: 83 | - write:pets 84 | - read:pets 85 | post: 86 | tags: 87 | - pet 88 | summary: Add a new pet to the store 89 | description: Add a new pet to the store 90 | operationId: addPet 91 | requestBody: 92 | description: Create a new pet in the store 93 | content: 94 | application/json: 95 | schema: 96 | $ref: "#/components/schemas/Pet" 97 | description: A Pet in JSON Format 98 | required: 99 | - id 100 | writeOnly: true 101 | application/xml: 102 | schema: 103 | $ref: "#/components/schemas/Pet" 104 | description: A Pet in XML Format 105 | required: 106 | - id 107 | writeOnly: true 108 | required: true 109 | responses: 110 | "200": 111 | description: Successful operation 112 | content: 113 | application/xml: 114 | schema: 115 | $ref: "#/components/schemas/Pet" 116 | description: A Pet in XML Format 117 | readOnly: true 118 | application/json: 119 | schema: 120 | $ref: "#/components/schemas/Pet" 121 | description: A Pet in JSON Format 122 | readOnly: true 123 | "405": 124 | description: Invalid input 125 | security: 126 | - petstore_auth: 127 | - write:pets 128 | - read:pets 129 | /pet/{petId}: 130 | get: 131 | tags: 132 | - pets 133 | summary: Find pet by ID 134 | description: 135 | Returns a pet when 0 < ID <= 10. ID > 10 or nonintegers will simulate 136 | API error conditions 137 | operationId: getPetById 138 | parameters: 139 | - name: petId 140 | in: path 141 | description: ID of pet that needs to be fetched 142 | required: true 143 | schema: 144 | type: integer 145 | format: int64 146 | description: param ID of pet that needs to be fetched 147 | exclusiveMaximum: 10 148 | exclusiveMinimum: 1 149 | responses: 150 | default: 151 | description: The pet 152 | content: 153 | application/json: 154 | schema: 155 | $ref: "#/components/schemas/Pet" 156 | description: A Pet in JSON format 157 | application/xml: 158 | schema: 159 | $ref: "#/components/schemas/Pet" 160 | description: A Pet in XML format 161 | "400": 162 | description: Invalid ID supplied 163 | "404": 164 | description: Pet not found 165 | security: 166 | - petstore_auth: 167 | - write:pets 168 | - read:pets 169 | - api_key: [] 170 | components: 171 | schemas: 172 | Category: 173 | $id: /components/schemas/category 174 | description: Category 175 | properties: 176 | id: 177 | type: integer 178 | format: int64 179 | example: 1 180 | name: 181 | type: string 182 | example: Dogs 183 | xml: 184 | name: Category 185 | Pet: 186 | $schema: https://json-schema.org/draft/2020-12/schema 187 | description: Pet 188 | properties: 189 | id: 190 | type: integer 191 | format: int64 192 | example: 10 193 | category: 194 | $ref: "#/components/schemas/Category" 195 | description: Pet Category 196 | name: 197 | type: string 198 | example: doggie 199 | photoUrls: 200 | type: array 201 | items: 202 | type: string 203 | xml: 204 | name: photoUrl 205 | xml: 206 | wrapped: true 207 | tags: 208 | type: array 209 | items: 210 | $ref: "#/components/schemas/Tag" 211 | xml: 212 | wrapped: true 213 | status: 214 | type: string 215 | description: pet status in the store 216 | enum: 217 | - available 218 | - pending 219 | - sold 220 | availableInstances: 221 | type: integer 222 | format: int32 223 | example: 7 224 | exclusiveMaximum: 10 225 | exclusiveMinimum: 1 226 | swagger-extension: true 227 | petDetailsId: 228 | type: integer 229 | format: int64 230 | $ref: /components/schemas/petdetails#pet_details_id 231 | petDetails: 232 | $ref: /components/schemas/petdetails 233 | required: 234 | - name 235 | - photoUrls 236 | xml: 237 | name: Pet 238 | PetDetails: 239 | $id: /components/schemas/petdetails 240 | $schema: https://json-schema.org/draft/2020-12/schema 241 | $vocabulary: https://spec.openapis.org/oas/3.1/schema-base 242 | properties: 243 | id: 244 | type: integer 245 | format: int64 246 | $anchor: pet_details_id 247 | example: 10 248 | category: 249 | $ref: /components/schemas/category 250 | description: PetDetails Category 251 | tag: 252 | $ref: /components/schemas/tag 253 | xml: 254 | name: PetDetails 255 | Tag: 256 | $id: /components/schemas/tag 257 | properties: 258 | id: 259 | type: integer 260 | format: int64 261 | name: 262 | type: string 263 | xml: 264 | name: Tag 265 | securitySchemes: 266 | petstore_auth: 267 | type: oauth2 268 | flows: 269 | implicit: 270 | authorizationUrl: https://petstore3.swagger.io/oauth/authorize 271 | scopes: 272 | write:pets: modify pets in your account 273 | read:pets: read your pets 274 | mutual_tls: 275 | type: mutualTLS 276 | api_key: 277 | type: apiKey 278 | name: api_key 279 | in: header 280 | webhooks: 281 | newPet: 282 | post: 283 | requestBody: 284 | description: Information about a new pet in the system 285 | content: 286 | application/json: 287 | schema: 288 | $ref: "#/components/schemas/Pet" 289 | description: Webhook Pet 290 | responses: 291 | "200": 292 | description: 293 | Return a 200 status to indicate that the data was received 294 | successfully 295 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------