Page MenuHomePhorge

No OneTemporary

Size
65 KB
Referenced Files
None
Subscribers
None
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..12179ea
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,20 @@
+# The directory Mix will write compiled artifacts to.
+/_build/
+
+# If you run "mix test --cover", coverage assets end up here.
+/cover/
+
+# The directory Mix downloads your dependencies sources to.
+/deps/
+
+# Where 3rd-party dependencies like ExDoc output generated docs.
+/doc/
+
+# Ignore .fetch files in case you like to edit your project deps locally.
+/.fetch
+
+# If the VM crashes, it generates a dump, let's ignore it too.
+erl_crash.dump
+
+# Also ignore archive artifacts (built via "mix archive.build").
+*.ez
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..be2cc4d
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,362 @@
+Mozilla Public License, version 2.0
+
+1. Definitions
+
+1.1. "Contributor"
+
+ means each individual or legal entity that creates, contributes to the
+ creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+
+ means the combination of the Contributions of others (if any) used by a
+ Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+
+ means Source Code Form to which the initial Contributor has attached the
+ notice in Exhibit A, the Executable Form of such Source Code Form, and
+ Modifications of such Source Code Form, in each case including portions
+ thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ a. that the initial Contributor has attached the notice described in
+ Exhibit B to the Covered Software; or
+
+ b. that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the terms of
+ a Secondary License.
+
+1.6. "Executable Form"
+
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+
+ means a work that combines Covered Software with other material, in a
+ separate file or files, that is not Covered Software.
+
+1.8. "License"
+
+ means this document.
+
+1.9. "Licensable"
+
+ means having the right to grant, to the maximum extent possible, whether
+ at the time of the initial grant or subsequently, any and all of the
+ rights conveyed by this License.
+
+1.10. "Modifications"
+
+ means any of the following:
+
+ a. any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered Software; or
+
+ b. any new file in Source Code Form that contains any Covered Software.
+
+1.11. "Patent Claims" of a Contributor
+
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the License,
+ by the making, using, selling, offering for sale, having made, import,
+ or transfer of either its Contributions or its Contributor Version.
+
+1.12. "Secondary License"
+
+ means either the GNU General Public License, Version 2.0, the GNU Lesser
+ General Public License, Version 2.1, the GNU Affero General Public
+ License, Version 3.0, or any later versions of those licenses.
+
+1.13. "Source Code Form"
+
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that controls, is
+ controlled by, or is under common control with You. For purposes of this
+ definition, "control" means (a) the power, direct or indirect, to cause
+ the direction or management of such entity, whether by contract or
+ otherwise, or (b) ownership of more than fifty percent (50%) of the
+ outstanding shares or beneficial ownership of such entity.
+
+
+2. License Grants and Conditions
+
+2.1. Grants
+
+ Each Contributor hereby grants You a world-wide, royalty-free,
+ non-exclusive license:
+
+ a. under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+ b. under Patent Claims of such Contributor to make, use, sell, offer for
+ sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+ The licenses granted in Section 2.1 with respect to any Contribution
+ become effective for each Contribution on the date the Contributor first
+ distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+ The licenses granted in this Section 2 are the only rights granted under
+ this License. No additional rights or licenses will be implied from the
+ distribution or licensing of Covered Software under this License.
+ Notwithstanding Section 2.1(b) above, no patent license is granted by a
+ Contributor:
+
+ a. for any code that a Contributor has removed from Covered Software; or
+
+ b. for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+ c. under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+ This License does not grant any rights in the trademarks, service marks,
+ or logos of any Contributor (except as may be necessary to comply with
+ the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+ No Contributor makes additional grants as a result of Your choice to
+ distribute the Covered Software under a subsequent version of this
+ License (see Section 10.2) or under the terms of a Secondary License (if
+ permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+ Each Contributor represents that the Contributor believes its
+ Contributions are its original creation(s) or it has sufficient rights to
+ grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+ This License is not intended to limit any rights You have under
+ applicable copyright doctrines of fair use, fair dealing, or other
+ equivalents.
+
+2.7. Conditions
+
+ Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
+ Section 2.1.
+
+
+3. Responsibilities
+
+3.1. Distribution of Source Form
+
+ All distribution of Covered Software in Source Code Form, including any
+ Modifications that You create or to which You contribute, must be under
+ the terms of this License. You must inform recipients that the Source
+ Code Form of the Covered Software is governed by the terms of this
+ License, and how they can obtain a copy of this License. You may not
+ attempt to alter or restrict the recipients' rights in the Source Code
+ Form.
+
+3.2. Distribution of Executable Form
+
+ If You distribute Covered Software in Executable Form then:
+
+ a. such Covered Software must also be made available in Source Code Form,
+ as described in Section 3.1, and You must inform recipients of the
+ Executable Form how they can obtain a copy of such Source Code Form by
+ reasonable means in a timely manner, at a charge no more than the cost
+ of distribution to the recipient; and
+
+ b. You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter the
+ recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+ You may create and distribute a Larger Work under terms of Your choice,
+ provided that You also comply with the requirements of this License for
+ the Covered Software. If the Larger Work is a combination of Covered
+ Software with a work governed by one or more Secondary Licenses, and the
+ Covered Software is not Incompatible With Secondary Licenses, this
+ License permits You to additionally distribute such Covered Software
+ under the terms of such Secondary License(s), so that the recipient of
+ the Larger Work may, at their option, further distribute the Covered
+ Software under the terms of either this License or such Secondary
+ License(s).
+
+3.4. Notices
+
+ You may not remove or alter the substance of any license notices
+ (including copyright notices, patent notices, disclaimers of warranty, or
+ limitations of liability) contained within the Source Code Form of the
+ Covered Software, except that You may alter any license notices to the
+ extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+ You may choose to offer, and to charge a fee for, warranty, support,
+ indemnity or liability obligations to one or more recipients of Covered
+ Software. However, You may do so only on Your own behalf, and not on
+ behalf of any Contributor. You must make it absolutely clear that any
+ such warranty, support, indemnity, or liability obligation is offered by
+ You alone, and You hereby agree to indemnify every Contributor for any
+ liability incurred by such Contributor as a result of warranty, support,
+ indemnity or liability terms You offer. You may include additional
+ disclaimers of warranty and limitations of liability specific to any
+ jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+
+ If it is impossible for You to comply with any of the terms of this License
+ with respect to some or all of the Covered Software due to statute,
+ judicial order, or regulation then You must: (a) comply with the terms of
+ this License to the maximum extent possible; and (b) describe the
+ limitations and the code they affect. Such description must be placed in a
+ text file included with all distributions of the Covered Software under
+ this License. Except to the extent prohibited by statute or regulation,
+ such description must be sufficiently detailed for a recipient of ordinary
+ skill to be able to understand it.
+
+5. Termination
+
+5.1. The rights granted under this License will terminate automatically if You
+ fail to comply with any of its terms. However, if You become compliant,
+ then the rights granted under this License from a particular Contributor
+ are reinstated (a) provisionally, unless and until such Contributor
+ explicitly and finally terminates Your grants, and (b) on an ongoing
+ basis, if such Contributor fails to notify You of the non-compliance by
+ some reasonable means prior to 60 days after You have come back into
+ compliance. Moreover, Your grants from a particular Contributor are
+ reinstated on an ongoing basis if such Contributor notifies You of the
+ non-compliance by some reasonable means, this is the first time You have
+ received notice of non-compliance with this License from such
+ Contributor, and You become compliant prior to 30 days after Your receipt
+ of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+ infringement claim (excluding declaratory judgment actions,
+ counter-claims, and cross-claims) alleging that a Contributor Version
+ directly or indirectly infringes any patent, then the rights granted to
+ You by any and all Contributors for the Covered Software under Section
+ 2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
+ license agreements (excluding distributors and resellers) which have been
+ validly granted by You or Your distributors under this License prior to
+ termination shall survive termination.
+
+6. Disclaimer of Warranty
+
+ Covered Software is provided under this License on an "as is" basis,
+ without warranty of any kind, either expressed, implied, or statutory,
+ including, without limitation, warranties that the Covered Software is free
+ of defects, merchantable, fit for a particular purpose or non-infringing.
+ The entire risk as to the quality and performance of the Covered Software
+ is with You. Should any Covered Software prove defective in any respect,
+ You (not any Contributor) assume the cost of any necessary servicing,
+ repair, or correction. This disclaimer of warranty constitutes an essential
+ part of this License. No use of any Covered Software is authorized under
+ this License except under this disclaimer.
+
+7. Limitation of Liability
+
+ Under no circumstances and under no legal theory, whether tort (including
+ negligence), contract, or otherwise, shall any Contributor, or anyone who
+ distributes Covered Software as permitted above, be liable to You for any
+ direct, indirect, special, incidental, or consequential damages of any
+ character including, without limitation, damages for lost profits, loss of
+ goodwill, work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses, even if such party shall have been
+ informed of the possibility of such damages. This limitation of liability
+ shall not apply to liability for death or personal injury resulting from
+ such party's negligence to the extent applicable law prohibits such
+ limitation. Some jurisdictions do not allow the exclusion or limitation of
+ incidental or consequential damages, so this exclusion and limitation may
+ not apply to You.
+
+8. Litigation
+
+ Any litigation relating to this License may be brought only in the courts
+ of a jurisdiction where the defendant maintains its principal place of
+ business and such litigation shall be governed by laws of that
+ jurisdiction, without reference to its conflict-of-law provisions. Nothing
+ in this Section shall prevent a party's ability to bring cross-claims or
+ counter-claims.
+
+9. Miscellaneous
+
+ This License represents the complete agreement concerning the subject
+ matter hereof. If any provision of this License is held to be
+ unenforceable, such provision shall be reformed only to the extent
+ necessary to make it enforceable. Any law or regulation which provides that
+ the language of a contract shall be construed against the drafter shall not
+ be used to construe this License against a Contributor.
+
+
+10. Versions of the License
+
+10.1. New Versions
+
+ Mozilla Foundation is the license steward. Except as provided in Section
+ 10.3, no one other than the license steward has the right to modify or
+ publish new versions of this License. Each version will be given a
+ distinguishing version number.
+
+10.2. Effect of New Versions
+
+ You may distribute the Covered Software under the terms of the version
+ of the License under which You originally received the Covered Software,
+ or under the terms of any subsequent version published by the license
+ steward.
+
+10.3. Modified Versions
+
+ If you create software not governed by this License, and you want to
+ create a new license for such software, you may create and use a
+ modified version of this License if you rename the license and remove
+ any references to the name of the license steward (except to note that
+ such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+ Licenses If You choose to distribute Source Code Form that is
+ Incompatible With Secondary Licenses under the terms of this version of
+ the License, the notice described in Exhibit B of this License must be
+ attached.
+
+Exhibit A - Source Code Form License Notice
+
+ This Source Code Form is subject to the
+ terms of the Mozilla Public License, v.
+ 2.0. If a copy of the MPL was not
+ distributed with this file, You can
+ obtain one at
+ http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular file,
+then You may include the notice in a location (such as a LICENSE file in a
+relevant directory) where a recipient would be likely to look for such a
+notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+
+ This Source Code Form is "Incompatible
+ With Secondary Licenses", as defined by
+ the Mozilla Public License, v. 2.0.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7193b0d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,149 @@
+# Open API Spex
+
+Add Open API Specification 3 (formerly swagger) to Plug applications.
+
+## Installation
+
+The package can be installed by adding `open_api_spex` to your list of dependencies in `mix.exs`:
+
+```elixir
+def deps do
+ [
+ {:open_api_spex, github: "mbuhot/open_api_spex"}
+ ]
+end
+```
+
+## Usage
+
+Start by adding an `ApiSpec` module to your application.
+
+```elixir
+defmodule MyApp.ApiSpec do
+ alias OpenApiSpex.{OpenApi, Server, Info, Paths}
+
+ def spec do
+ %OpenApi{
+ servers: [
+ # Populate the Server info from a phoenix endpoint
+ Server.from_endpoint(MyAppWeb.Endpoint, otp_app: :my_app)
+ ],
+ info: %Info{
+ title: "My App",
+ version: "1.0"
+ },
+ # populate the paths from a phoenix router
+ paths: Paths.from_router(MyAppWeb.Router)
+ }
+ |> OpenApiSpex.resolve_schema_modules() # discover request/response schemas from path specs
+ end
+end
+```
+
+For each plug (controller) that will handle api requests, add an `open_api_operation` callback.
+It will be passed the plug opts that were declared in the router, this will be the action for a phoenix controller.
+
+```elixir
+defmodule MyApp.UserController do
+ alias OpenApiSpex.Operation
+
+ @spec open_api_operation(any) :: Operation.t
+ def open_api_operation(action), do: apply(__MODULE__, :"#{action}_operation", [])
+
+ @spec show_operation() :: Operation.t
+ def show_operation() do
+
+ %Operation{
+ tags: ["users"],
+ summary: "Show user",
+ description: "Show a user by ID",
+ operationId: "UserController.show",
+ parameters: [
+ Operation.parameter(:id, :path, :integer, "User ID", example: 123)
+ ],
+ responses: %{
+ 200 => Operation.response("User", "application/json", Schemas.UserResponse)
+ }
+ }
+ end
+ def show(conn, %{"id" => id}) do
+ {:ok, user} = MyApp.Users.find_by_id(id)
+ json(conn, 200, user)
+ end
+end
+```
+
+Declare the JSON schemas for request/response bodies in a `Schemas` module:
+
+```elixir
+defmodule MyApp.Schemas do
+ alias OpenApiSpex.Schema
+
+ defmodule User do
+ def schema do
+ %Schema{
+ title: "User",
+ description: "A user of the app",
+ type: :object,
+ properties: %{
+ id: %Schema{type: :integer, description: "User ID"},
+ name: %Schema{type: :string, description: "User name"},
+ email: %Schema{type: :string, description: "Email address", format: :email},
+ inserted_at: %Schema{type: :string, description: "Creation timestamp", format: :datetime},
+ updated_at: %Schema{type: :string, description: "Update timestamp", format: :datetime}
+ }
+ }
+ end
+ end
+
+ defmodule UserResponse do
+ def schema do
+ %Schema{
+ title: "UserResponse",
+ description: "Response schema for single user",
+ type: :object,
+ properties: %{
+ data: User
+ }
+ }
+ end
+ end
+end
+```
+
+Now you can create a mix task to write the swagger file to disk:
+
+```elixir
+defmodule Mix.Tasks.MyApp.OpenApiSpec do
+ def run([output_file]) do
+ json =
+ MyApp.ApiSpec.spec()
+ |> Poison.encode!(pretty: true)
+
+ :ok = File.write!(output_file, json)
+ end
+end
+```
+
+Generate the file with: `mix myapp.openapispec spec.json`
+
+You can also serve the swagger through a controller:
+
+```elixir
+defmodule MyApp.OpenApiSpecController do
+ def show(conn, _params) do
+ spec =
+ MyApp.ApiSpec.spec()
+
+ json(conn, spec)
+ end
+end
+```
+
+TODO: SwaggerUI 3.0
+
+TODO: Request Validation
+
+TODO: Validating examples in the spec
+
+TODO: Validating responses in tests
diff --git a/config/config.exs b/config/config.exs
new file mode 100644
index 0000000..1c990ef
--- /dev/null
+++ b/config/config.exs
@@ -0,0 +1,30 @@
+# This file is responsible for configuring your application
+# and its dependencies with the aid of the Mix.Config module.
+use Mix.Config
+
+# This configuration is loaded before any dependency and is restricted
+# to this project. If another project depends on this project, this
+# file won't be loaded nor affect the parent project. For this reason,
+# if you want to provide default values for your application for
+# 3rd-party users, it should be done in your "mix.exs" file.
+
+# You can configure your application as:
+#
+# config :open_api_spex, key: :value
+#
+# and access this configuration in your application as:
+#
+# Application.get_env(:open_api_spex, :key)
+#
+# You can also configure a 3rd-party app:
+#
+# config :logger, level: :info
+#
+
+# It is also possible to import configuration files, relative to this
+# directory. For example, you can emulate configuration per environment
+# by uncommenting the line below and defining dev.exs, test.exs and such.
+# Configuration from the imported file will override the ones defined
+# here (which is why it is important to import them last).
+#
+# import_config "#{Mix.env}.exs"
diff --git a/lib/open_api_spex.ex b/lib/open_api_spex.ex
new file mode 100644
index 0000000..428ae2d
--- /dev/null
+++ b/lib/open_api_spex.ex
@@ -0,0 +1,11 @@
+defmodule OpenApiSpex do
+ alias OpenApiSpex.{OpenApi, SchemaResolver}
+
+ @moduledoc """
+ """
+ def resolve_schema_modules(spec = %OpenApi{}) do
+ SchemaResolver.resolve_schema_modules(spec)
+ end
+
+
+end
diff --git a/lib/open_api_spex/callback.ex b/lib/open_api_spex/callback.ex
new file mode 100644
index 0000000..38d4d50
--- /dev/null
+++ b/lib/open_api_spex/callback.ex
@@ -0,0 +1,6 @@
+defmodule OpenApiSpex.Callback do
+ alias OpenApiSpex.PathItem
+ @type t :: %{
+ String.t => PathItem.t
+ }
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/components.ex b/lib/open_api_spex/components.ex
new file mode 100644
index 0000000..40b7c62
--- /dev/null
+++ b/lib/open_api_spex/components.ex
@@ -0,0 +1,28 @@
+defmodule OpenApiSpex.Components do
+ alias OpenApiSpex.{
+ Schema, Reference, Response, Parameter, Example,
+ RequestBody, Header, SecurityScheme, Link, Callback
+ }
+ defstruct [
+ :schemas,
+ :responses,
+ :parameters,
+ :examples,
+ :requestBodies,
+ :headers,
+ :securitySchemes,
+ :links,
+ :callbacks,
+ ]
+ @type t :: %{
+ schemas: %{String.t => Schema.t | Reference.t},
+ responses: %{String.t => Response.t | Reference.t},
+ parameters: %{String.t => Parameter.t | Reference.t},
+ examples: %{String.t => Example.t | Reference.t},
+ requestBodies: %{String.t => RequestBody.t | Reference.t},
+ headers: %{String.t => Header.t | Reference.t},
+ securitySchemes: %{String.t => SecurityScheme.t | Reference.t},
+ links: %{String.t => Link.t | Reference.t},
+ callbacks: %{String.t => Callback.t | Reference.t}
+ }
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/contact.ex b/lib/open_api_spex/contact.ex
new file mode 100644
index 0000000..edbe286
--- /dev/null
+++ b/lib/open_api_spex/contact.ex
@@ -0,0 +1,12 @@
+defmodule OpenApiSpex.Contact do
+ defstruct [
+ :name,
+ :url,
+ :email
+ ]
+ @type t :: %__MODULE__{
+ name: String.t,
+ url: String.t,
+ email: String.t
+ }
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/discriminator.ex b/lib/open_api_spex/discriminator.ex
new file mode 100644
index 0000000..45a9b1c
--- /dev/null
+++ b/lib/open_api_spex/discriminator.ex
@@ -0,0 +1,10 @@
+defmodule OpenApiSpex.Discriminator do
+ defstruct [
+ :propertyName,
+ :mapping
+ ]
+ @type t :: %__MODULE__{
+ propertyName: String.t,
+ mapping: %{String.t => String.t}
+ }
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/encoding.ex b/lib/open_api_spex/encoding.ex
new file mode 100644
index 0000000..aa4ddb6
--- /dev/null
+++ b/lib/open_api_spex/encoding.ex
@@ -0,0 +1,17 @@
+defmodule OpenApiSpex.Encoding do
+ alias OpenApiSpex.{Header, Reference, Parameter}
+ defstruct [
+ :contentType,
+ :headers,
+ :style,
+ :explode,
+ :allowReserved
+ ]
+ @type t :: %__MODULE__{
+ contentType: String.t,
+ headers: %{String.t => Header.t | Reference.t},
+ style: Parameter.style,
+ explode: boolean,
+ allowReserved: boolean
+ }
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/example.ex b/lib/open_api_spex/example.ex
new file mode 100644
index 0000000..a36e0eb
--- /dev/null
+++ b/lib/open_api_spex/example.ex
@@ -0,0 +1,14 @@
+defmodule OpenApiSpex.Example do
+ defstruct [
+ :summary,
+ :description,
+ :value,
+ :externalValue
+ ]
+ @type t :: %{
+ summary: String.t,
+ description: String.t,
+ value: any,
+ externalValue: String.t
+ }
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/external_documentation.ex b/lib/open_api_spex/external_documentation.ex
new file mode 100644
index 0000000..9af2574
--- /dev/null
+++ b/lib/open_api_spex/external_documentation.ex
@@ -0,0 +1,10 @@
+defmodule OpenApiSpex.ExternalDocumentation do
+ defstruct [
+ :description,
+ :url
+ ]
+ @type t :: %__MODULE__{
+ description: String.t,
+ url: String.t
+ }
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/header.ex b/lib/open_api_spex/header.ex
new file mode 100644
index 0000000..c1380e8
--- /dev/null
+++ b/lib/open_api_spex/header.ex
@@ -0,0 +1,25 @@
+defmodule OpenApiSpex.Header do
+ alias OpenApiSpex.{Schema, Reference, Example}
+ defstruct [
+ :description,
+ :required,
+ :deprecated,
+ :allowEmptyValue,
+ :explode,
+ :schema,
+ :example,
+ :examples,
+ style: :simple
+ ]
+ @type t :: %__MODULE__{
+ description: String.t,
+ required: boolean,
+ deprecated: boolean,
+ allowEmptyValue: boolean,
+ style: :simple,
+ explode: boolean,
+ schema: Schema.t | Reference.t,
+ example: any,
+ examples: %{String.t => Example.t | Reference.t}
+ }
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/info.ex b/lib/open_api_spex/info.ex
new file mode 100644
index 0000000..8950197
--- /dev/null
+++ b/lib/open_api_spex/info.ex
@@ -0,0 +1,20 @@
+defmodule OpenApiSpex.Info do
+ alias OpenApiSpex.{Contact, License}
+ @enforce_keys [:title, :version]
+ defstruct [
+ :title,
+ :description,
+ :termsOfService,
+ :contact,
+ :license,
+ :version
+ ]
+ @type t :: %__MODULE__{
+ title: String.t,
+ description: String.t,
+ termsOfService: String.t,
+ contact: Contact.t,
+ license: License.t,
+ version: String.t
+ }
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/license.ex b/lib/open_api_spex/license.ex
new file mode 100644
index 0000000..4e013f4
--- /dev/null
+++ b/lib/open_api_spex/license.ex
@@ -0,0 +1,10 @@
+defmodule OpenApiSpex.License do
+ defstruct [
+ :name,
+ :url
+ ]
+ @type t :: %__MODULE__{
+ name: String.t,
+ url: String.t
+ }
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/link.ex b/lib/open_api_spex/link.ex
new file mode 100644
index 0000000..9b2aa15
--- /dev/null
+++ b/lib/open_api_spex/link.ex
@@ -0,0 +1,19 @@
+defmodule OpenApiSpex.Link do
+ alias OpenApiSpex.Server
+ defstruct [
+ :operationRef,
+ :operationId,
+ :parameters,
+ :requestBody,
+ :description,
+ :server
+ ]
+ @type t :: %{
+ operationRef: String.t,
+ operationId: String.t,
+ parameters: %{String.t => any},
+ requestBody: any,
+ description: String.t,
+ server: Server.t
+ }
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/media_type.ex b/lib/open_api_spex/media_type.ex
new file mode 100644
index 0000000..0045133
--- /dev/null
+++ b/lib/open_api_spex/media_type.ex
@@ -0,0 +1,15 @@
+defmodule OpenApiSpex.MediaType do
+ alias OpenApiSpex.{Schema, Reference, Example, Encoding}
+ defstruct [
+ :schema,
+ :example,
+ :examples,
+ :encoding
+ ]
+ @type t :: %__MODULE__{
+ schema: Schema.t | Reference.t,
+ example: any,
+ examples: %{String.t => Example.t | Reference.t},
+ encoding: %{String => Encoding.t}
+ }
+end
diff --git a/lib/open_api_spex/oauth_flow.ex b/lib/open_api_spex/oauth_flow.ex
new file mode 100644
index 0000000..84cac43
--- /dev/null
+++ b/lib/open_api_spex/oauth_flow.ex
@@ -0,0 +1,14 @@
+defmodule OpenApiSpex.OAuthFlow do
+ defstruct [
+ :authorizationUrl,
+ :tokenUrl,
+ :refreshUrl,
+ :scopes
+ ]
+ @type t :: %__MODULE__{
+ authorizationUrl: String.t,
+ tokenUrl: String.t,
+ refreshUrl: String.t,
+ scopes: %{String.t => String.t}
+ }
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/oauth_flows.ex b/lib/open_api_spex/oauth_flows.ex
new file mode 100644
index 0000000..abebedf
--- /dev/null
+++ b/lib/open_api_spex/oauth_flows.ex
@@ -0,0 +1,15 @@
+defmodule OpenApiSpex.OAuthFlows do
+ alias OpenApiSpex.OAuthFlow
+ defstruct [
+ :implicit,
+ :password,
+ :clientCredentials,
+ :authorizationCode
+ ]
+ @type t :: %__MODULE__{
+ implicit: OAuthFlow.t,
+ password: OAuthFlow.t,
+ clientCredentials: OAuthFlow.t,
+ authorizationCode: OAuthFlow.t
+ }
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/open_api.ex b/lib/open_api_spex/open_api.ex
new file mode 100644
index 0000000..0322d26
--- /dev/null
+++ b/lib/open_api_spex/open_api.ex
@@ -0,0 +1,55 @@
+defmodule OpenApiSpex.OpenApi do
+ alias OpenApiSpex.{
+ Info, Server, Paths, Components,
+ SecurityRequirement, Tag, ExternalDocumentation,
+ OpenApi
+ }
+ defstruct [
+ :info,
+ :servers,
+ :paths,
+ :components,
+ :security,
+ :tags,
+ :externalDocs,
+ openapi: "3.0",
+ ]
+ @type t :: %OpenApi{
+ openapi: String.t,
+ info: Info.t,
+ servers: [Server.t],
+ paths: Paths.t,
+ components: Components.t,
+ security: [SecurityRequirement.t],
+ tags: [Tag.t],
+ externalDocs: ExternalDocumentation.t
+ }
+
+ defimpl Poison.Encoder do
+ def encode(api_spec = %OpenApi{}, options) do
+ api_spec
+ |> to_json()
+ |> Poison.Encoder.encode(options)
+ end
+
+ defp to_json(value = %{__struct__: _}) do
+ value
+ |> Map.from_struct()
+ |> to_json()
+ end
+ defp to_json(value) when is_map(value) do
+ value
+ |> Enum.map(fn {k,v} -> {to_string(k), to_json(v)} end)
+ |> Enum.filter(fn {_, nil} -> false; _ -> true end)
+ |> Enum.into(%{})
+ end
+ defp to_json(value) when is_list(value) do
+ Enum.map(value, &to_json/1)
+ end
+ defp to_json(nil) do nil end
+ defp to_json(true) do true end
+ defp to_json(false) do false end
+ defp to_json(value) when is_atom(value) do to_string(value) end
+ defp to_json(value) do value end
+ end
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/operation.ex b/lib/open_api_spex/operation.ex
new file mode 100644
index 0000000..b10357c
--- /dev/null
+++ b/lib/open_api_spex/operation.ex
@@ -0,0 +1,102 @@
+defmodule OpenApiSpex.Operation do
+ alias OpenApiSpex.{
+ ExternalDocumentation, Parameter, Reference,
+ RequestBody, Responses, Callback,
+ SecurityRequirement, Server, MediaType, Response
+ }
+
+ defstruct [
+ :tags,
+ :summary,
+ :description,
+ :externalDocs,
+ :operationId,
+ :parameters,
+ :requestBody,
+ :responses,
+ :callbacks,
+ :deprecated,
+ :security,
+ :servers
+ ]
+ @type t :: %__MODULE__{
+ tags: [String.t],
+ summary: String.t,
+ description: String.t,
+ externalDocs: ExternalDocumentation.t,
+ operationId: String.t,
+ parameters: [Parameter.t | Reference.t],
+ requestBody: [RequestBody.t | Reference.t],
+ responses: Responses.t,
+ callbacks: %{
+ String.t => Callback.t | Reference.t
+ },
+ deprecated: boolean,
+ security: [SecurityRequirement.t],
+ servers: [Server.t]
+ }
+
+ @doc """
+ Constructs an Operation struct from the plug and opts specified in the given route
+ """
+ @spec from_route(PathItem.route) :: t
+ def from_route(route) do
+ from_plug(route.plug, route.opts)
+ end
+
+ @doc """
+ Constructs an Operation struct from plug module and opts
+ """
+ @spec from_plug(module, any) :: t
+ def from_plug(plug, opts) do
+ plug.open_api_operation(opts)
+ end
+
+ @doc """
+ Shorthand for constructing a Parameter name, location, type, description and optional examples
+ """
+ @spec parameter(String.t, String.t, String.t, keyword) :: RequestBody.t
+ def parameter(name, location, type, description, opts \\ []) do
+ params =
+ [name: name, in: location, description: description, required: location == :path]
+ |> Keyword.merge(opts)
+
+ Parameter
+ |> struct(params)
+ |> Parameter.put_schema(type)
+ end
+
+ @doc """
+ Shorthand for constructing a RequestBody with description, media_type, schema and optional examples
+ """
+ @spec request_body(String.t, String.t, String.t, keyword) :: RequestBody.t
+ def request_body(description, media_type, schema_ref, opts \\ []) do
+ %RequestBody{
+ description: description,
+ content: %{
+ media_type => %MediaType{
+ schema: schema_ref,
+ example: opts[:example],
+ examples: opts[:examples]
+ }
+ }
+ }
+ end
+
+ @doc """
+ Shorthand for constructing a Response with description, media_type, schema and optional examples
+ """
+ @spec response(String.t, String.t, String.t, keyword) :: Response.t
+ def response(description, media_type, schema_ref, opts \\ []) do
+ %Response{
+ description: description,
+ content: %{
+ media_type => %MediaType {
+ schema: schema_ref,
+ example: opts[:example],
+ examples: opts[:examples]
+ }
+ }
+ }
+ end
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/parameter.ex b/lib/open_api_spex/parameter.ex
new file mode 100644
index 0000000..4d195bd
--- /dev/null
+++ b/lib/open_api_spex/parameter.ex
@@ -0,0 +1,50 @@
+defmodule OpenApiSpex.Parameter do
+ alias OpenApiSpex.{
+ Schema, Reference, Example, MediaType, Parameter
+ }
+ defstruct [
+ :name,
+ :in,
+ :description,
+ :required,
+ :deprecated,
+ :allowEmptyValue,
+ :style,
+ :explode,
+ :allowReserved,
+ :schema,
+ :example,
+ :examples,
+ :content,
+ ]
+ @type style :: :matrix | :label | :form | :simple | :spaceDelimited | :pipeDelimited | :deep
+ @type t :: %__MODULE__{
+ name: String.t,
+ in: :query | :header | :path | :cookie,
+ description: String.t,
+ required: boolean,
+ deprecated: boolean,
+ allowEmptyValue: boolean,
+ style: style,
+ explode: boolean,
+ allowReserved: boolean,
+ schema: Schema.t | Reference.t,
+ example: any,
+ examples: %{String.t => Example.t | Reference.t},
+ content: %{String.t => MediaType.t}
+ }
+
+ @doc """
+ Sets the schema for a parameter from a simple type, reference or Schema
+ """
+ @spec put_schema(t, Reference.t | Schema.t | atom | String.t) :: t
+ def put_schema(parameter = %Parameter{}, type = %Reference{}) do
+ %{parameter | schema: type}
+ end
+ def put_schema(parameter = %Parameter{}, type = %Schema{}) do
+ %{parameter | schema: type}
+ end
+ def put_schema(parameter = %Parameter{}, type) when is_binary(type) or is_atom(type) do
+ %{parameter | schema: %Schema{type: type}}
+ end
+end
diff --git a/lib/open_api_spex/path_item.ex b/lib/open_api_spex/path_item.ex
new file mode 100644
index 0000000..84a6b04
--- /dev/null
+++ b/lib/open_api_spex/path_item.ex
@@ -0,0 +1,57 @@
+defmodule OpenApiSpex.PathItem do
+ alias OpenApiSpex.{Operation, Server, Parameter, PathItem, Reference}
+ defstruct [
+ :"$ref",
+ :summary,
+ :description,
+ :get,
+ :put,
+ :post,
+ :delete,
+ :options,
+ :head,
+ :patch,
+ :trace,
+ :servers,
+ :parameters
+ ]
+ @type t :: %__MODULE__{
+ "$ref": String.t,
+ summary: String.t,
+ description: String.t,
+ get: Operation.t,
+ put: Operation.t,
+ post: Operation.t,
+ delete: Operation.t,
+ options: Operation.t,
+ head: Operation.t,
+ patch: Operation.t,
+ trace: Operation.t,
+ servers: [Server.t],
+ parameters: [Parameter.t | Reference.t]
+ }
+
+ @type route :: %{verb: atom, plug: atom, opts: any}
+
+ @doc """
+ Builds a PathItem struct from a list of routes that share a path.
+ """
+ @spec from_routes([route]) :: nil | t
+ def from_routes(routes) do
+ Enum.each(routes, fn route ->
+ Code.ensure_loaded(route.plug)
+ end)
+
+ routes
+ |> Enum.filter(&function_exported?(&1.plug, :open_api_operation, 1))
+ |> from_valid_routes()
+ end
+
+ @spec from_valid_routes([route]) :: nil | t
+ defp from_valid_routes([]), do: nil
+ defp from_valid_routes(routes) do
+ Enum.reduce(routes, %PathItem{}, fn route, path_item ->
+ Map.put(path_item, route.verb, Operation.from_route(route))
+ end)
+ end
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/paths.ex b/lib/open_api_spex/paths.ex
new file mode 100644
index 0000000..1e9c304
--- /dev/null
+++ b/lib/open_api_spex/paths.ex
@@ -0,0 +1,25 @@
+defmodule OpenApiSpex.Paths do
+ alias OpenApiSpex.PathItem
+
+ @type t :: %{String.t => PathItem.t}
+
+ @doc """
+ Create a Paths map from the routes in the given router module.
+ """
+ @spec from_router(module) :: t
+ def from_router(router) do
+ router.__routes__()
+ |> Enum.group_by(fn route -> route.path end)
+ |> Enum.map(fn {k, v} -> {open_api_path(k), PathItem.from_routes(v)} end)
+ |> Enum.filter(fn {_k, v} -> !is_nil(v) end)
+ |> Map.new()
+ end
+
+ @spec open_api_path(String.t) :: String.t
+ defp open_api_path(path) do
+ path
+ |> String.split("/")
+ |> Enum.map(fn ":"<>segment -> "{#{segment}}"; segment -> segment end)
+ |> Enum.join("/")
+ end
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/reference.ex b/lib/open_api_spex/reference.ex
new file mode 100644
index 0000000..6359bf1
--- /dev/null
+++ b/lib/open_api_spex/reference.ex
@@ -0,0 +1,8 @@
+defmodule OpenApiSpex.Reference do
+ defstruct [
+ :"$ref"
+ ]
+ @type t :: %{
+ "$ref": String.t
+ }
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/request_body.ex b/lib/open_api_spex/request_body.ex
new file mode 100644
index 0000000..0bf2255
--- /dev/null
+++ b/lib/open_api_spex/request_body.ex
@@ -0,0 +1,13 @@
+defmodule OpenApiSpex.RequestBody do
+ alias OpenApiSpex.MediaType
+ defstruct [
+ :description,
+ :content,
+ :required
+ ]
+ @type t :: %__MODULE__{
+ description: String.t,
+ content: %{String.t => MediaType.t},
+ required: boolean
+ }
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/response.ex b/lib/open_api_spex/response.ex
new file mode 100644
index 0000000..b9da420
--- /dev/null
+++ b/lib/open_api_spex/response.ex
@@ -0,0 +1,15 @@
+defmodule OpenApiSpex.Response do
+ alias OpenApiSpex.{Header, Reference, MediaType, Link}
+ defstruct [
+ :description,
+ :headers,
+ :content,
+ :links
+ ]
+ @type t :: %__MODULE__{
+ description: String.t,
+ headers: %{String.t => Header.t | Reference.t},
+ content: %{String.t => MediaType.t},
+ links: %{String.t => Link.t | Reference.t}
+ }
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/responses.ex b/lib/open_api_spex/responses.ex
new file mode 100644
index 0000000..9100257
--- /dev/null
+++ b/lib/open_api_spex/responses.ex
@@ -0,0 +1,7 @@
+defmodule OpenApiSpex.Responses do
+ alias OpenApiSpex.{Response, Reference}
+ @type t :: %{
+ :default => Response.t | Reference.t,
+ integer => Response.t | Reference.t
+ }
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/schema.ex b/lib/open_api_spex/schema.ex
new file mode 100644
index 0000000..78ea548
--- /dev/null
+++ b/lib/open_api_spex/schema.ex
@@ -0,0 +1,79 @@
+defmodule OpenApiSpex.Schema do
+ alias OpenApiSpex.{
+ Schema, Reference, Discriminator, Xml, ExternalDocumentation
+ }
+ defstruct [
+ :title,
+ :multipleOf,
+ :maximum,
+ :exclusiveMaximum,
+ :minimum,
+ :exclusiveMinimum,
+ :maxLength,
+ :minLength,
+ :pattern,
+ :maxItems,
+ :minItems,
+ :uniqueItems,
+ :maxProperties,
+ :minProperties,
+ :required,
+ :enum,
+ :type,
+ :allOf,
+ :oneOf,
+ :anyOf,
+ :not,
+ :items,
+ :properties,
+ :additionalProperties,
+ :description,
+ :format,
+ :default,
+ :nullable,
+ :discriminator,
+ :readOnly,
+ :writeOnly,
+ :xml,
+ :externalDocs,
+ :example,
+ :deprecated
+ ]
+ @type t :: %__MODULE__{
+ title: String.t,
+ multipleOf: number,
+ maximum: number,
+ exclusiveMaximum: number,
+ minimum: number,
+ exclusiveMinimum: number,
+ maxLength: integer,
+ minLength: integer,
+ pattern: String.t,
+ maxItems: integer,
+ minItems: integer,
+ uniqueItems: boolean,
+ maxProperties: integer,
+ minProperties: integer,
+ required: [String.t],
+ enum: [String.t],
+ type: String.t,
+ allOf: [Schema.t | Reference.t],
+ oneOf: [Schema.t | Reference.t],
+ anyOf: [Schema.t | Reference.t],
+ not: Schema.t | Reference.t,
+ items: Schema.t | Reference.t,
+ properties: %{String.t => Schema.t | Reference.t},
+ additionalProperties: boolean | Schema.t | Reference.t,
+ description: String.t,
+ format: String.t,
+ default: any,
+ nullable: boolean,
+ discriminator: Discriminator.t,
+ readOnly: boolean,
+ writeOnly: boolean,
+ xml: Xml.t,
+ externalDocs: ExternalDocumentation.t,
+ example: any,
+ deprecated: boolean
+ }
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/schema_resolver.ex b/lib/open_api_spex/schema_resolver.ex
new file mode 100644
index 0000000..c007d74
--- /dev/null
+++ b/lib/open_api_spex/schema_resolver.ex
@@ -0,0 +1,159 @@
+defmodule OpenApiSpex.SchemaResolver do
+ alias OpenApiSpex.{
+ OpenApi,
+ Components,
+ PathItem,
+ Operation,
+ Parameter,
+ Reference,
+ MediaType,
+ Schema,
+ RequestBody,
+ Response
+ }
+
+ def resolve_schema_modules(spec = %OpenApi{}) do
+ components = spec.components || %Components{}
+ schemas = components.schemas || %{}
+ {paths, schemas} = resolve_schema_modules_from_paths(spec.paths, schemas)
+ schemas = resolve_schema_modules_from_schemas(schemas)
+ %{spec | paths: paths, components: %{components| schemas: schemas}}
+ end
+
+ def resolve_schema_modules_from_paths(paths = %{}, schemas = %{}) do
+ Enum.reduce(paths, {paths, schemas}, fn {path, path_item}, {paths, schemas} ->
+ {new_path_item, schemas} = resolve_schema_modules_from_path_item(path_item, schemas)
+ {Map.put(paths, path, new_path_item), schemas}
+ end)
+ end
+
+ def resolve_schema_modules_from_path_item(path = %PathItem{}, schemas) do
+ path
+ |> Map.from_struct()
+ |> Enum.filter(fn {_k, v} -> match?(%Operation{}, v) end)
+ |> Enum.reduce({path, schemas}, fn {k, operation}, {path, schemas} ->
+ {new_operation, schemas} = resolve_schema_modules_from_operation(operation, schemas)
+ {Map.put(path, k, new_operation), schemas}
+ end)
+ end
+
+ def resolve_schema_modules_from_operation(operation = %Operation{}, schemas) do
+ {parameters, schemas} = resolve_schema_modules_from_parameters(operation.parameters, schemas)
+ {request_body, schemas} = resolve_schema_modules_from_request_body(operation.requestBody, schemas)
+ {responses, schemas} = resolve_schema_modules_from_responses(operation.responses, schemas)
+ new_operation = %{operation | parameters: parameters, requestBody: request_body, responses: responses}
+ {new_operation, schemas}
+ end
+
+ def resolve_schema_modules_from_parameters(nil, schemas), do: {nil, schemas}
+ def resolve_schema_modules_from_parameters(parameters, schemas) do
+ {parameters, schemas} =
+ Enum.reduce(parameters, {[], schemas}, fn parameter, {parameters, schemas} ->
+ {new_parameter, schemas} = resolve_schema_modules_from_parameter(parameter, schemas)
+ {[new_parameter | parameters], schemas}
+ end)
+ {Enum.reverse(parameters), schemas}
+ end
+
+ def resolve_schema_modules_from_parameter(parameter = %Parameter{schema: schema, content: nil}, schemas) when is_atom(schema) do
+ {ref, new_schemas} = resolve_schema_modules_from_schema(schema, schemas)
+ new_parameter = %{parameter | schema: ref}
+ {new_parameter, new_schemas}
+ end
+ def resolve_schema_modules_from_parameter(parameter = %Parameter{schema: nil, content: content = %{}}, schemas) do
+ {new_content, schemas} = resolve_schema_modules_from_content(content, schemas)
+ {%{parameter | content: new_content}, schemas}
+ end
+ def resolve_schema_modules_from_parameter(parameter = %Parameter{}, schemas) do
+ {parameter, schemas}
+ end
+
+ def resolve_schema_modules_from_content(nil, schemas), do: {nil, schemas}
+ def resolve_schema_modules_from_content(content, schemas) do
+ Enum.reduce(content, {content, schemas}, fn {mime, media}, {content, schemas} ->
+ {new_media, schemas} = resolve_schema_modules_from_media_type(media, schemas)
+ {Map.put(content, mime, new_media), schemas}
+ end)
+ end
+
+ def resolve_schema_modules_from_media_type(media = %MediaType{schema: schema}, schemas) when is_atom(schema) do
+ {ref, new_schemas} = resolve_schema_modules_from_schema(schema, schemas)
+ new_media = %{media | schema: ref}
+ {new_media, new_schemas}
+ end
+ def resolve_schema_modules_from_media_type(media = %MediaType{}, schemas) do
+ {media, schemas}
+ end
+
+ def resolve_schema_modules_from_request_body(nil, schemas), do: {nil, schemas}
+ def resolve_schema_modules_from_request_body(request_body = %RequestBody{}, schemas) do
+ {content, schemas} = resolve_schema_modules_from_content(request_body.content, schemas)
+ new_request_body = %{request_body | content: content}
+ {new_request_body, schemas}
+ end
+
+ def resolve_schema_modules_from_responses(responses = %{}, schemas = %{}) do
+ Enum.reduce(responses, {responses, schemas}, fn {status, response}, {responses, schemas} ->
+ {new_response, schemas} = resolve_schema_modules_from_response(response, schemas)
+ {Map.put(responses, status, new_response), schemas}
+ end)
+ end
+
+ def resolve_schema_modules_from_response(response = %Response{}, schemas = %{}) do
+ {content, schemas} = resolve_schema_modules_from_content(response.content, schemas)
+ new_response = %{response | content: content}
+ {new_response, schemas}
+ end
+
+ def resolve_schema_modules_from_schemas(schemas = %{}) do
+ Enum.reduce(schemas, schemas, fn {name, schema}, schemas ->
+ {schema, schemas} = resolve_schema_modules_from_schema(schema, schemas)
+ Map.put(schemas, name, schema)
+ end)
+ end
+
+ def resolve_schema_modules_from_schema(false, schemas), do: {false, schemas}
+ def resolve_schema_modules_from_schema(true, schemas), do: {true, schemas}
+ def resolve_schema_modules_from_schema(nil, schemas), do: {nil, schemas}
+ def resolve_schema_modules_from_schema(schema, schemas) when is_atom(schema) do
+ title = schema.schema().title
+ new_schemas = cond do
+ Map.has_key?(schemas, title) ->
+ schemas
+
+ true ->
+ {new_schema, schemas} = resolve_schema_modules_from_schema(schema.schema(), schemas)
+ Map.put(schemas, title, new_schema)
+ end
+ {%Reference{"$ref": "#/components/schemas/#{title}"}, new_schemas}
+ end
+ def resolve_schema_modules_from_schema(schema = %Schema{}, schemas) do
+ {all_of, schemas} = resolve_schema_modules_from_schema(schema.allOf, schemas)
+ {one_of, schemas} = resolve_schema_modules_from_schema(schema.oneOf, schemas)
+ {any_of, schemas} = resolve_schema_modules_from_schema(schema.anyOf, schemas)
+ {not_schema, schemas} = resolve_schema_modules_from_schema(schema.not, schemas)
+ {items, schemas} = resolve_schema_modules_from_schema(schema.items, schemas)
+ {additional, schemas} = resolve_schema_modules_from_schema(schema.additionalProperties, schemas)
+ {properties, schemas} = resolve_schema_modules_from_schema_properties(schema.properties, schemas)
+ schema =
+ %{schema |
+ allOf: all_of,
+ oneOf: one_of,
+ anyOf: any_of,
+ not: not_schema,
+ items: items,
+ additionalProperties: additional,
+ properties: properties
+ }
+ {schema, schemas}
+ end
+ def resolve_schema_modules_from_schema(ref = %Reference{}, schemas), do: {ref, schemas}
+
+ def resolve_schema_modules_from_schema_properties(nil, schemas), do: {nil, schemas}
+ def resolve_schema_modules_from_schema_properties(properties, schemas) do
+ Enum.reduce(properties, {properties, schemas}, fn {name, property}, {properties, schemas} ->
+ {new_property, schemas} = resolve_schema_modules_from_schema(property, schemas)
+ {Map.put(properties, name, new_property), schemas}
+ end)
+ end
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/security_requirement.ex b/lib/open_api_spex/security_requirement.ex
new file mode 100644
index 0000000..efabc17
--- /dev/null
+++ b/lib/open_api_spex/security_requirement.ex
@@ -0,0 +1,3 @@
+defmodule OpenApiSpex.SecurityRequirement do
+ @type t :: %{String.t => [String.t]}
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/security_scheme.ex b/lib/open_api_spex/security_scheme.ex
new file mode 100644
index 0000000..ab74baf
--- /dev/null
+++ b/lib/open_api_spex/security_scheme.ex
@@ -0,0 +1,23 @@
+defmodule OpenApiSpex.SecurityScheme do
+ alias OpenApiSpex.OAuthFlows
+ defstruct [
+ :type,
+ :description,
+ :name,
+ :in,
+ :scheme,
+ :bearerFormat,
+ :flows,
+ :openIdConnectUrl
+ ]
+ @type t :: %__MODULE__{
+ type: String.t,
+ description: String.t,
+ name: String.t,
+ in: String.t,
+ scheme: String.t,
+ bearerFormat: String.t,
+ flows: OAuthFlows.t,
+ openIdConnectUrl: String.t
+ }
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/server.ex b/lib/open_api_spex/server.ex
new file mode 100644
index 0000000..c8cb5be
--- /dev/null
+++ b/lib/open_api_spex/server.ex
@@ -0,0 +1,28 @@
+defmodule OpenApiSpex.Server do
+ alias OpenApiSpex.{Server, ServerVariable}
+ defstruct [
+ :url,
+ :description,
+ variables: %{}
+ ]
+ @type t :: %Server{
+ url: String.t,
+ description: String.t,
+ variables: %{String.t => ServerVariable.t}
+ }
+
+ @doc """
+ Builds a Server from a phoenix Endpoint module
+ """
+ @spec from_endpoint(module, keyword) :: t
+ def from_endpoint(endpoint, otp_app: app) do
+ url_config = Application.get_env(app, endpoint, []) |> Keyword.get(:url, [])
+ scheme = Keyword.get(url_config, :scheme, "http")
+ host = Keyword.get(url_config, :host, "localhost")
+ port = Keyword.get(url_config, :port, "80")
+ path = Keyword.get(url_config, :path, "/")
+ %Server{
+ url: "#{scheme}://#{host}:#{port}#{path}"
+ }
+ end
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/servier_variable.ex b/lib/open_api_spex/servier_variable.ex
new file mode 100644
index 0000000..20426c0
--- /dev/null
+++ b/lib/open_api_spex/servier_variable.ex
@@ -0,0 +1,12 @@
+defmodule OpenApiSpex.ServerVariable do
+ defstruct [
+ :enum,
+ :default,
+ :description
+ ]
+ @type t :: %{
+ enum: [String.t],
+ default: String.t,
+ description: String.t
+ }
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/tag.ex b/lib/open_api_spex/tag.ex
new file mode 100644
index 0000000..80a964c
--- /dev/null
+++ b/lib/open_api_spex/tag.ex
@@ -0,0 +1,13 @@
+defmodule OpenApiSpex.Tag do
+ alias OpenApiSpex.ExternalDocumentation
+ defstruct [
+ :name,
+ :description,
+ :externalDocs
+ ]
+ @type t :: %{
+ name: String.t,
+ description: String.t,
+ externalDocs: ExternalDocumentation.t
+ }
+end
\ No newline at end of file
diff --git a/lib/open_api_spex/xml.ex b/lib/open_api_spex/xml.ex
new file mode 100644
index 0000000..1980898
--- /dev/null
+++ b/lib/open_api_spex/xml.ex
@@ -0,0 +1,16 @@
+defmodule OpenApiSpex.Xml do
+ defstruct [
+ :name,
+ :namespace,
+ :prefix,
+ :attribute,
+ :wrapped
+ ]
+ @type t :: %__MODULE__{
+ name: String.t,
+ namespace: String.t,
+ prefix: String.t,
+ attribute: boolean,
+ wrapped: boolean
+ }
+end
\ No newline at end of file
diff --git a/mix.exs b/mix.exs
new file mode 100644
index 0000000..8e11943
--- /dev/null
+++ b/mix.exs
@@ -0,0 +1,32 @@
+defmodule OpenApiSpex.Mixfile do
+ use Mix.Project
+
+ def project do
+ [
+ app: :open_api_spex,
+ version: "0.1.0",
+ elixir: "~> 1.5",
+ elixirc_paths: elixirc_paths(Mix.env),
+ start_permanent: Mix.env == :prod,
+ deps: deps()
+ ]
+ end
+
+ defp elixirc_paths(:test), do: ["lib", "test/support"]
+ defp elixirc_paths(_), do: ["lib"]
+
+ # Run "mix help compile.app" to learn about applications.
+ def application do
+ [
+ extra_applications: [:logger]
+ ]
+ end
+
+ # Run "mix help deps" to learn about dependencies.
+ defp deps do
+ [
+ {:poison, ">= 0.0.0"},
+ {:phoenix, "~> 1.3", only: :test, runtime: false}
+ ]
+ end
+end
diff --git a/mix.lock b/mix.lock
new file mode 100644
index 0000000..fea52f7
--- /dev/null
+++ b/mix.lock
@@ -0,0 +1,5 @@
+%{"mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [], [], "hexpm"},
+ "phoenix": {:hex, :phoenix, "1.3.0", "1c01124caa1b4a7af46f2050ff11b267baa3edb441b45dbf243e979cd4c5891b", [], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
+ "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [], [], "hexpm"},
+ "plug": {:hex, :plug, "1.4.3", "236d77ce7bf3e3a2668dc0d32a9b6f1f9b1f05361019946aae49874904be4aed", [], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"},
+ "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [], [], "hexpm"}}
diff --git a/test/open_api_spex_test.exs b/test/open_api_spex_test.exs
new file mode 100644
index 0000000..1f49b47
--- /dev/null
+++ b/test/open_api_spex_test.exs
@@ -0,0 +1,11 @@
+defmodule OpenApiSpexTest do
+ use ExUnit.Case
+ alias OpenApiSpexTest.ApiSpec
+
+ describe "OpenApi" do
+ test "compete" do
+ spec = ApiSpec.spec()
+ assert spec
+ end
+ end
+end
\ No newline at end of file
diff --git a/test/operation_test.exs b/test/operation_test.exs
new file mode 100644
index 0000000..5800126
--- /dev/null
+++ b/test/operation_test.exs
@@ -0,0 +1,16 @@
+defmodule OpenApiSpex.OperationTest do
+ use ExUnit.Case
+ alias OpenApiSpex.Operation
+ alias OpenApiSpexTest.UserController
+
+ describe "Operation" do
+ test "from_route" do
+ route = %{plug: UserController, opts: :show}
+ assert Operation.from_route(route) == UserController.show_operation()
+ end
+
+ test "from_plug" do
+ assert Operation.from_plug(UserController, :show) == UserController.show_operation()
+ end
+ end
+end
\ No newline at end of file
diff --git a/test/path_item_test.exs b/test/path_item_test.exs
new file mode 100644
index 0000000..16ffb62
--- /dev/null
+++ b/test/path_item_test.exs
@@ -0,0 +1,21 @@
+defmodule OpenApiSpex.PathItemTest do
+ use ExUnit.Case
+ alias OpenApiSpex.PathItem
+ alias OpenApiSpexTest.{Router, UserController}
+
+ describe "PathItem" do
+ @tag :focus
+ test "from_routes" do
+ routes =
+ for route <- Router.__routes__(),
+ route.path == "/api/users",
+ do: route
+
+ path_item = PathItem.from_routes(routes)
+ assert path_item == %PathItem{
+ get: UserController.index_operation(),
+ post: UserController.create_operation()
+ }
+ end
+ end
+end
\ No newline at end of file
diff --git a/test/paths_test.exs b/test/paths_test.exs
new file mode 100644
index 0000000..f740129
--- /dev/null
+++ b/test/paths_test.exs
@@ -0,0 +1,14 @@
+defmodule OpenApiSpex.PathsTest do
+ use ExUnit.Case
+ alias OpenApiSpex.{Paths, PathItem}
+ alias OpenApiSpexTest.Router
+
+ describe "Paths" do
+ test "from_router" do
+ paths = Paths.from_router(Router)
+ assert %{
+ "/api/users" => %PathItem{},
+ } = paths
+ end
+ end
+end
\ No newline at end of file
diff --git a/test/schema_resolver_test.exs b/test/schema_resolver_test.exs
new file mode 100644
index 0000000..f99e896
--- /dev/null
+++ b/test/schema_resolver_test.exs
@@ -0,0 +1,70 @@
+defmodule OpenApiSpex.SchemaResolverTest do
+ use ExUnit.Case
+ alias OpenApiSpex.{
+ MediaType,
+ OpenApi,
+ Operation,
+ PathItem,
+ Reference,
+ RequestBody,
+ Response,
+ Schema
+ }
+
+ test "Resolves schemas in OpenApi spec" do
+ spec = %OpenApi{
+ paths: %{
+ "/api/users" => %PathItem{
+ get: %Operation{
+ responses: %{
+ 200 => %Response{
+ content: %{
+ "application/json" => %MediaType{
+ schema: OpenApiSpexTest.Schemas.UsersResponse
+ }
+ }
+ }
+ }
+ },
+ post: %Operation{
+ description: "Create a user",
+ operationId: "UserController.create",
+ requestBody: %RequestBody{
+ content: %{
+ "application/json" => %MediaType{
+ schema: OpenApiSpexTest.Schemas.UserRequest
+ }
+ }
+ },
+ responses: %{
+ 201 => %Response{
+ content: %{
+ "application/json" => %MediaType{
+ schema: OpenApiSpexTest.Schemas.UserResponse
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ resolved = OpenApiSpex.resolve_schema_modules(spec)
+
+ assert %Reference{"$ref": "#/components/schemas/UsersReponse"} =
+ resolved.paths["/api/users"].get.responses[200].content["application/json"].schema
+
+ assert %Reference{"$ref": "#/components/schemas/UserResponse"} =
+ resolved.paths["/api/users"].post.responses[201].content["application/json"].schema
+
+ assert %Reference{"$ref": "#/components/schemas/UserRequest"} =
+ resolved.paths["/api/users"].post.requestBody.content["application/json"].schema
+
+ assert %{
+ "UserRequest" => %Schema{},
+ "UserResponse" => %Schema{},
+ "User" => %Schema{},
+ } = resolved.components.schemas
+ end
+end
\ No newline at end of file
diff --git a/test/server_test.exs b/test/server_test.exs
new file mode 100644
index 0000000..0cff345
--- /dev/null
+++ b/test/server_test.exs
@@ -0,0 +1,20 @@
+defmodule OpenApiSpex.ServerTest do
+ use ExUnit.Case
+ alias OpenApiSpex.{Server}
+ alias OpenApiSpexText.Endpoint
+
+ describe "Server" do
+ test "from_endpoint" do
+ Application.put_env(:phoenix_swagger, Endpoint, [
+ url: [host: "example.com", port: 1234, path: "/api/v1/", scheme: :https],
+ ])
+
+ server = Server.from_endpoint(Endpoint, otp_app: :phoenix_swagger)
+
+ assert %{
+ url: "https://example.com:1234/api/v1/"
+ } = server
+ end
+ end
+
+end
\ No newline at end of file
diff --git a/test/support/api_spec.ex b/test/support/api_spec.ex
new file mode 100644
index 0000000..6018841
--- /dev/null
+++ b/test/support/api_spec.ex
@@ -0,0 +1,27 @@
+defmodule OpenApiSpexTest.ApiSpec do
+ alias OpenApiSpex.{OpenApi, Contact, License, Paths, Server, Info}
+ alias OpenApiSpexTest.Router
+
+ def spec() do
+ %OpenApi{
+ servers: [
+ %Server{url: "http://example.com"},
+ ],
+ info: %Info{
+ title: "A",
+ version: "3.0",
+ contact: %Contact{
+ name: "joe",
+ email: "Joe@gmail.com",
+ url: "https://help.joe.com"
+ },
+ license: %License{
+ name: "MIT",
+ url: "http://mit.edu/license"
+ }
+ },
+ paths: Paths.from_router(Router)
+ }
+ |> OpenApiSpex.resolve_schema_modules()
+ end
+end
\ No newline at end of file
diff --git a/test/support/open_api_spec_controller.ex b/test/support/open_api_spec_controller.ex
new file mode 100644
index 0000000..6827c88
--- /dev/null
+++ b/test/support/open_api_spec_controller.ex
@@ -0,0 +1,8 @@
+defmodule OpenApiSpexTest.OpenApiSpecController do
+ alias OpenApiSpexTest.ApiSpec
+
+ def init(:show), do: :show
+ def call(conn, :show) do
+ Phoenix.Controller.json(conn, ApiSpec.spec())
+ end
+end
\ No newline at end of file
diff --git a/test/support/router.ex b/test/support/router.ex
new file mode 100644
index 0000000..666dfe9
--- /dev/null
+++ b/test/support/router.ex
@@ -0,0 +1,8 @@
+defmodule OpenApiSpexTest.Router do
+ use Phoenix.Router
+
+ scope "/api", OpenApiSpexTest do
+ resources "/users", UserController, only: [:create, :index, :show]
+ get "/openapi", OpenApiSpecController, :show
+ end
+end
\ No newline at end of file
diff --git a/test/support/schemas.ex b/test/support/schemas.ex
new file mode 100644
index 0000000..88eb6fe
--- /dev/null
+++ b/test/support/schemas.ex
@@ -0,0 +1,59 @@
+defmodule OpenApiSpexTest.Schemas do
+ alias OpenApiSpex.Schema
+
+ defmodule User do
+ def schema do
+ %Schema{
+ title: "User",
+ description: "A user of the app",
+ type: :object,
+ properties: %{
+ id: %Schema{type: :integer, description: "User ID"},
+ name: %Schema{type: :string, description: "User name"},
+ email: %Schema{type: :string, description: "Email address", format: :email},
+ inserted_at: %Schema{type: :string, description: "Creation timestamp", format: :datetime},
+ updated_at: %Schema{type: :string, description: "Update timestamp", format: :datetime}
+ }
+ }
+ end
+ end
+
+ defmodule UserRequest do
+ def schema do
+ %Schema{
+ title: "UserRequest",
+ description: "POST body for creating a user",
+ type: :object,
+ properties: %{
+ user: User
+ }
+ }
+ end
+ end
+
+ defmodule UserResponse do
+ def schema do
+ %Schema{
+ title: "UserResponse",
+ description: "Response schema for single user",
+ type: :object,
+ properties: %{
+ data: User
+ }
+ }
+ end
+ end
+
+ defmodule UsersResponse do
+ def schema do
+ %Schema{
+ title: "UsersReponse",
+ description: "Response schema for multiple users",
+ type: :object,
+ properties: %{
+ data: %Schema{description: "The users details", type: :array, items: User}
+ }
+ }
+ end
+ end
+end
\ No newline at end of file
diff --git a/test/support/user_controller.ex b/test/support/user_controller.ex
new file mode 100644
index 0000000..1b66f44
--- /dev/null
+++ b/test/support/user_controller.ex
@@ -0,0 +1,66 @@
+defmodule OpenApiSpexTest.UserController do
+ use Phoenix.Controller
+ alias OpenApiSpex.Operation
+ alias OpenApiSpexTest.Schemas
+
+ def open_api_operation(action) do
+ apply(__MODULE__, :"#{action}_operation", [])
+ end
+
+ def show_operation() do
+ import Operation
+ %Operation{
+ tags: ["users"],
+ summary: "Show user",
+ description: "Show a user by ID",
+ operationId: "UserController.show",
+ parameters: [
+ parameter(:id, :path, :integer, "User ID", example: 123)
+ ],
+ responses: %{
+ 200 => response("User", "application/json", Schemas.UserResponse)
+ }
+ }
+ end
+ def show(conn, _params) do
+ conn
+ |> Plug.Conn.send_resp(200, "HELLO")
+ end
+
+ def index_operation() do
+ import Operation
+ %Operation{
+ tags: ["users"],
+ summary: "List users",
+ description: "List all useres",
+ operationId: "UserController.index",
+ parameters: [],
+ responses: %{
+ 200 => response("User List Response", "application/json", Schemas.UsersResponse)
+ }
+ }
+ end
+ def index(conn, _params) do
+ conn
+ |> Plug.Conn.send_resp(200, "HELLO")
+ end
+
+ def create_operation() do
+ import Operation
+ %Operation{
+ tags: ["users"],
+ summary: "Create user",
+ description: "Create a user",
+ operationId: "UserController.create",
+ parameters: [],
+ requestBody: request_body("The user attributes", "application/json", Schemas.UserRequest),
+ responses: %{
+ 201 => response("User", "application/json", Schemas.UserResponse)
+ }
+ }
+ end
+ def create(conn, _params) do
+ conn
+ |> Plug.Conn.send_resp(201, "DONE")
+ end
+end
\ No newline at end of file
diff --git a/test/test_helper.exs b/test/test_helper.exs
new file mode 100644
index 0000000..869559e
--- /dev/null
+++ b/test/test_helper.exs
@@ -0,0 +1 @@
+ExUnit.start()

File Metadata

Mime Type
text/x-diff
Expires
Sun, Nov 24, 5:27 AM (22 h, 19 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
39351
Default Alt Text
(65 KB)

Event Timeline