Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions lib/mimicry/mock_api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ defmodule Mimicry.MockAPI do
conn
|> get_req_header("x-mimicry-expect-status")
|> List.first()
# TODO: For JSON, all keys are strings, always
|> ensure_numerical()
|> fallback("default")

expected_example =
Expand Down Expand Up @@ -94,13 +92,4 @@ defmodule Mimicry.MockAPI do
body: value
}
end

defp ensure_numerical(nil), do: nil

defp ensure_numerical(maybe_number) do
case Integer.parse(maybe_number) do
:error -> nil
{number, _} -> number
end
end
end
27 changes: 24 additions & 3 deletions lib/mimicry/open_api/parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ defmodule Mimicry.OpenAPI.Parser do
def json(str), do: parse(str, :json)

@doc """
parses a given string depending on its extension
parses a given string based on the extension to a Mimicry.Specifaction

If you need a map instead of the specification, see parse_to_map/2
"""
@spec parse(String.t(), :yaml | :json) :: Specification.t()
@spec parse(term(), :yaml | :json) :: Specification.t()
Comment on lines -23 to +25
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for this spec change?
Also shouldn't parse/2 and parse_to_map/2 have the same definition for the inputs?

def parse(str, atom) do
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this function could now be shortened to something like this:

def parse(str, atom) do
  str
  |> parse_to_map(atom)
  |> case do
    {:ok, decoded} ->
      decoded |> build_specification()
    {:error, _err} ->
      Specification.unsupported()
  end
end

str
|> decoder(atom).()
Expand All @@ -35,10 +37,28 @@ defmodule Mimicry.OpenAPI.Parser do
end
end

@doc """
parses a given string to a map
"""
@spec parse_to_map(any(), :yaml | :json) :: :error | any()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the doc says string, shouldn't it be also in the spec?

Suggested change
@spec parse_to_map(any(), :yaml | :json) :: :error | any()
@spec parse_to_map(String.t(), :yaml | :json) :: :error | any()

def parse_to_map(str, atom) do
str
|> decoder(atom).()
|> case do
{:ok, decoded} ->
decoded

{:error, err} ->
Logger.warn("Could not decode #{atom |> to_string() |> String.upcase()} specification")
Logger.error(err)
:error
end
end

@doc """
builds a new Specification from inputs given
"""
@spec build_specification(map()) :: Specification.t()
@spec build_specification(any()) :: Specification.t()
Comment on lines -41 to +61
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question as above about spec changes.

def build_specification(
parsed = %{
"info" => info,
Expand All @@ -65,6 +85,7 @@ defmodule Mimicry.OpenAPI.Parser do
# NOTE: This cannot read multiple specifications in a YAML file as of yet
:yaml -> &YamlElixir.read_from_string/1
:json -> &Jason.decode/1
_ -> raise RuntimeError, "Unknown extension: #{atom}"
end
end
end
2 changes: 1 addition & 1 deletion lib/mimicry/open_api/response.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ defmodule Mimicry.OpenAPI.Response do
@moduledoc """
A struct to represent a potential response based on the `Mimicry.OpenAPI.Specification` powering the `MockServer`.
"""
defstruct [:content_type, :schema, :description, examples: %{}, status: 200]
defstruct [:content_type, :schema, :description, examples: %{}, status: "200"]
end
9 changes: 8 additions & 1 deletion lib/mimicry_api/controllers/proxy_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ defmodule MimicryApi.ProxyController do
conn
|> clean_default_headers()
|> merge_resp_headers(headers)
|> put_status(status)
|> put_status(status |> as_integer())
|> json(body)
end

Expand All @@ -63,4 +63,11 @@ defmodule MimicryApi.ProxyController do
end

defp get_host(%Specification{}), do: nil

defp as_integer(val) do
case Integer.parse(val) do
{num, _} -> num
_ -> raise RuntimeError, message: "Cannot convert #{val} to proper response code"
end
end
end
45 changes: 36 additions & 9 deletions lib/mimicry_api/controllers/server_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,60 @@ defmodule MimicryApi.ServerController do

alias Mimicry.{MockServer, MockServerList}
alias Mimicry.OpenAPI.Parser, as: SpecParser
alias MimicryApi.Errors

def index(conn, _params) do
conn |> json(%{servers: MockServerList.list_servers()})
end

def create(conn, %{"spec" => definition}) do
definition |> SpecParser.build_specification()
def create(conn, %{"json" => raw_definition}) do
spec = raw_definition |> SpecParser.parse_to_map(:json)

with spec <- definition |> SpecParser.build_specification(),
_create(conn, spec)
end

def create(conn, %{"yaml" => raw_definition}) do
spec = raw_definition |> SpecParser.parse_to_map(:yaml)
_create(conn, spec)
end

def create(conn, _) do
conn
|> put_status(:unprocessable_entity)
|> json(%{message: "Pass the raw spec either via 'yaml' or 'json' properties"})
end

defp _create(conn, definition) do
with :ok <- definition |> OpenAPIv3Validator.validate(),
spec <- definition |> SpecParser.build_specification(),
{:ok, pid} <- MockServerList.create_server(spec),
{:ok, %{id: id} = response} <- pid |> MockServer.get_details() do
conn
|> put_resp_header("x-mimicry-server-id", id |> to_string())
|> json(response)
else
{:error, errors} ->
conn
|> put_status(:bad_request)
|> json(%{
message: "Invalid specification",
errors: errors |> Errors.make_errors_from_openapi_validation()
})

{:error, :invalid_spec} ->
conn |> put_status(:bad_request) |> json(%{message: "Invalid specification!"})

{:error, _} ->
conn |> put_status(:bad_request) |> json(%{message: "Invalid JSON"})
end
end

def create(conn, _) do
conn
|> put_status(:bad_request)
|> json(%{message: "Missing 'spec' property!"})
rescue
FunctionClauseError ->
conn
|> put_status(:unprocessable_entity)
|> json(%{
message:
"Could not parse specification. Please use strings for response codes instead of integers."
})
end

def spec(conn = %Plug.Conn{req_headers: headers}, _params) do
Expand Down
8 changes: 8 additions & 0 deletions lib/mimicry_api/errors.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule MimicryApi.Errors do
@moduledoc """
Provides functions for transforming errors from libs into something usable to the API
"""
def make_errors_from_openapi_validation(errors) do
errors |> Enum.map(fn {msg, path} -> %{path => msg} end)
end
end
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ defmodule Mimicry.MixProject do
{:phoenix, "~> 1.5"},
{:plug_cowboy, "~> 2.4"},
{:yaml_elixir, "~> 2.6"},
{:openapiv3_validator, "~> 0.1.0"},

# dev
{:mix_test_watch, "~> 1.0", only: :dev, runtime: false},
Expand Down
2 changes: 2 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"earmark_parser": {:hex, :earmark_parser, "1.4.15", "b29e8e729f4aa4a00436580dcc2c9c5c51890613457c193cc8525c388ccb2f06", [:mix], [], "hexpm", "044523d6438ea19c1b8ec877ec221b008661d3c27e3b848f4c879f500421ca5c"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"ex_doc": {:hex, :ex_doc, "0.25.2", "4f1cae793c4d132e06674b282f1d9ea3bf409bcca027ddb2fe177c4eed6a253f", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "5b0c172e87ac27f14dfd152d52a145238ec71a95efbf29849550278c58a393d6"},
"ex_json_schema": {:hex, :ex_json_schema, "0.8.1", "d4cf78f318e7eea9ed173b4bb74b24a2d1601a7da5b5a857c07fe3d5da8f7fb6", [:mix], [], "hexpm", "4c870cfd422f6f5f4a6944cffde8eff029ba27b7f4783d94ed0f97ace27811d1"},
"excoveralls": {:hex, :excoveralls, "0.14.2", "f9f5fd0004d7bbeaa28ea9606251bb643c313c3d60710bad1f5809c845b748f0", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "ca6fd358621cb4d29311b29d4732c4d47dac70e622850979bc54ed9a3e50f3e1"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"},
Expand All @@ -25,6 +26,7 @@
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mix_test_watch": {:hex, :mix_test_watch, "1.1.0", "330bb91c8ed271fe408c42d07e0773340a7938d8a0d281d57a14243eae9dc8c3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "52b6b1c476cbb70fd899ca5394506482f12e5f6b0d6acff9df95c7f1e0812ec3"},
"nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
"openapiv3_validator": {:hex, :openapiv3_validator, "0.1.0", "ebcfe821bb29ab6259612286aba72795264a9e5b68d7f8f02117fb0459cfda6d", [:mix], [{:ex_json_schema, "~> 0.8.1", [hex: :ex_json_schema, repo: "hexpm", optional: false]}], "hexpm", "ed3f63aad49415c93ad3cd980cb12942f18de19b2a247ad3e0ca83d4c0a327fb"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"phoenix": {:hex, :phoenix, "1.5.12", "75fddb14c720388eea93d33886166a690416a7ff8633fbd93f364355b6fe1166", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8f0ae6734fcc18bbaa646c161e2febc46fb899eae43f82679b92530983324113"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
Expand Down
66 changes: 66 additions & 0 deletions test/fixtures/specs/simple-with-broken-integer.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
openapi: 3.0.3
info:
title: 'Test YAML'
version: '1.0'
license:
name: MIT
description: 'A simple API'
servers:
- url: https://simple-api.testing.com
description: production
paths:
'/':
get:
responses:
default:
description: 'The default response for this endpoint'
content:
application/json:
schema:
$ref: '#/components/schemas/SimpleMessage'
examples:
simple-reference:
$ref: '#/components/examples/simple-message-example'
simple-embedded:
summary: 'An embedded example'
value:
message: 'embedded message example'
simple-combined-example:
$ref: '#/components/examples/simple-message-example'
summary: 'simple example with overriden summary'
404: # <- this is the problem, while technically correct YAML, this will break the parser, since it need compat with JSON
description: The response for when something is not found
content:
application/json:
schema:
$ref: '#/components/schemas/SimpleError'
examples:
simple-error:
$ref: '#/components/examples/simple-error'

components:
schemas:
SimpleMessage:
title: 'A simple message'
description: 'an example of a simple message'
type: object
properties:
message:
type: string
SimpleError:
title: 'A simple Error'
description: 'A simple error model'
type: object
properties:
code:
type: integer
examples:
simple-message-example:
summary: 'a simple message'
value:
message: 'foobar'
simple-error:
summary: 'a simple error'
value:
code: 42

95 changes: 90 additions & 5 deletions test/fixtures/specs/simple.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"openapi": "3.0.0",
"openapi": "3.0.3",
"info": {
"title": "Test JSON",
"title": "Test YAML",
"version": "1.0",
"license": {
"name": "MIT"
Expand All @@ -10,9 +10,94 @@
},
"servers": [
{
"url": "https://simple-api.com",
"url": "https://simple-api.testing.com",
"description": "production"
}
],
"paths": []
}
"paths": {
"/": {
"get": {
"responses": {
"404": {
"description": "The response for when something is not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SimpleError"
},
"examples": {
"simple-error": {
"$ref": "#/components/examples/simple-error"
}
}
}
}
},
"default": {
"description": "The default response for this endpoint",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SimpleMessage"
},
"examples": {
"simple-reference": {
"$ref": "#/components/examples/simple-message-example"
},
"simple-embedded": {
"summary": "An embedded example",
"value": {
"message": "embedded message example"
}
},
"simple-combined-example": {
"$ref": "#/components/examples/simple-message-example",
"summary": "simple example with overriden summary"
}
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"SimpleMessage": {
"title": "A simple message",
"description": "an example of a simple message",
"type": "object",
"properties": {
"message": {
"type": "string"
}
}
},
"SimpleError": {
"title": "A simple Error",
"description": "A simple error model",
"type": "object",
"properties": {
"code": {
"type": "integer"
}
}
}
},
"examples": {
"simple-message-example": {
"summary": "a simple message",
"value": {
"message": "foobar"
}
},
"simple-error": {
"summary": "a simple error",
"value": {
"code": 42
}
}
}
}
}
2 changes: 1 addition & 1 deletion test/fixtures/specs/simple.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ paths:
simple-combined-example:
$ref: '#/components/examples/simple-message-example'
summary: 'simple example with overriden summary'
404:
'404':
description: The response for when something is not found
content:
application/json:
Expand Down
Loading