ropen_pi 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rubocop.yml +4 -0
- data/.rubocop_todo.yml +54 -0
- data/.ruby-version +1 -0
- data/.solargraph.yml +10 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +177 -0
- data/LICENSE +21 -0
- data/README.md +53 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/examples/.envrc +14 -0
- data/examples/Gemfile +28 -0
- data/examples/Gemfile.lock +234 -0
- data/examples/docker-compose.yml +86 -0
- data/examples/resources/docker/docker-entrypoint.sh +15 -0
- data/lib/generators/ropen_pi/USAGE +8 -0
- data/lib/generators/ropen_pi/install_generator.rb +11 -0
- data/lib/generators/ropen_pi/templates/ropen_pi_helper.rb +25 -0
- data/lib/ropen_pi.rb +5 -0
- data/lib/ropen_pi/config_helper.rb +134 -0
- data/lib/ropen_pi/specs.rb +23 -0
- data/lib/ropen_pi/specs/configuration.rb +41 -0
- data/lib/ropen_pi/specs/example_group_helpers.rb +284 -0
- data/lib/ropen_pi/specs/example_helpers.rb +20 -0
- data/lib/ropen_pi/specs/extended_schema.rb +30 -0
- data/lib/ropen_pi/specs/open_api_formatter.rb +100 -0
- data/lib/ropen_pi/specs/railtie.rb +7 -0
- data/lib/ropen_pi/specs/request_factory.rb +178 -0
- data/lib/ropen_pi/specs/response_validator.rb +67 -0
- data/lib/ropen_pi/specs/writer.rb +35 -0
- data/lib/ropen_pi/version.rb +4 -0
- data/lib/tasks/ropen_pi.rake +10 -0
- data/ropen_pi.gemspec +36 -0
- metadata +192 -0
@@ -0,0 +1,86 @@
|
|
1
|
+
version: '3.7'
|
2
|
+
|
3
|
+
# shared variables to keep things DRY
|
4
|
+
x-shared-env: &shared
|
5
|
+
DB_HOST: ${DB_HOST}
|
6
|
+
DB_USER: ${DB_USER}
|
7
|
+
DB_NAME: ${DB_NAME}
|
8
|
+
DB_PASSWORD: ${DB_PASSWORD}
|
9
|
+
SVC_NAME: ${SVC_NAME}
|
10
|
+
SVC_SHORT: ${SVC_SHORT}
|
11
|
+
|
12
|
+
networks:
|
13
|
+
net__backend: ~
|
14
|
+
|
15
|
+
netext__traefik:
|
16
|
+
external: true
|
17
|
+
|
18
|
+
volumes:
|
19
|
+
# saves the gems as a mount that's used by docker
|
20
|
+
bundle: ~
|
21
|
+
|
22
|
+
services:
|
23
|
+
svc:
|
24
|
+
container_name: ${SVC_NAME}
|
25
|
+
depends_on:
|
26
|
+
- db
|
27
|
+
entrypoint: resources/docker/docker-entrypoint.sh
|
28
|
+
environment:
|
29
|
+
<<: *shared
|
30
|
+
image: talentplatforms/ruby:${RUBY_IMAGE_VERSION}
|
31
|
+
labels:
|
32
|
+
- org.label-schema.name=${SVC_NAME}
|
33
|
+
- org.label-schema.description=${SVC_DESCRIPTION}
|
34
|
+
- org.label-schema.version=1.0.0
|
35
|
+
networks:
|
36
|
+
- net__backend
|
37
|
+
stdin_open: true
|
38
|
+
# the tmp folder should not be kept, so it's an ephemeral mount
|
39
|
+
# this also speeds up requests
|
40
|
+
tmpfs:
|
41
|
+
- /app/log
|
42
|
+
- /app/tmp
|
43
|
+
tty: true
|
44
|
+
volumes:
|
45
|
+
- .:/app:cached
|
46
|
+
- bundle:/bundle
|
47
|
+
|
48
|
+
db:
|
49
|
+
container_name: ${DB_HOST}
|
50
|
+
environment:
|
51
|
+
POSTGRES_USER: ${DB_USER}
|
52
|
+
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
53
|
+
POSTGRES_DB: ${DB_NAME}
|
54
|
+
image: postgres:10-alpine
|
55
|
+
networks:
|
56
|
+
- net__backend
|
57
|
+
ports:
|
58
|
+
# the db port to be used by external guis. this also needs to be changed
|
59
|
+
- ${DB_EXPOSED_PORT}:5432
|
60
|
+
|
61
|
+
# this is just a development helper that displays the
|
62
|
+
# created openApi documentation
|
63
|
+
# https://github.com/swagger-api/swagger-ui/tree/master/docs
|
64
|
+
# swagger:
|
65
|
+
# container_name: ${SVC_NAME}-swagger
|
66
|
+
# environment:
|
67
|
+
# URLS: "[{ url: \"${SWAGGER_ENDPOINT}\", name: \"${SVC_NAME}\" }]"
|
68
|
+
# image: swaggerapi/swagger-ui:v3.23.9
|
69
|
+
# labels:
|
70
|
+
# # traefik stuff
|
71
|
+
# - traefik.enable=true
|
72
|
+
# - traefik.docker.network=netext__traefik
|
73
|
+
# # http
|
74
|
+
# - traefik.http.routers.${SVC_NAME}-swagger.rule=Host(`swagger.${SVC_NAME}.localhost`)
|
75
|
+
# - traefik.http.routers.${SVC_NAME}-swagger.entrypoints=http
|
76
|
+
# - traefik.http.routers.${SVC_NAME}-swagger.service=${SVC_NAME}-swagger
|
77
|
+
# - traefik.http.routers.${SVC_NAME}-swagger.middlewares.redirect=redirect@file
|
78
|
+
# # https
|
79
|
+
# - traefik.http.routers.${SVC_NAME}-swagger-ssl.rule=Host(`swagger.${SVC_NAME}.localhost`)
|
80
|
+
# - traefik.http.routers.${SVC_NAME}-swagger-ssl.entrypoints=https
|
81
|
+
# - traefik.http.routers.${SVC_NAME}-swagger-ssl.tls=true
|
82
|
+
# - traefik.http.services.${SVC_NAME}-swagger.loadbalancer.server.port=8080
|
83
|
+
# networks:
|
84
|
+
# - netext__traefik
|
85
|
+
# volumes:
|
86
|
+
# - ./resources/open-api:/swagger
|
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
set -ex
|
4
|
+
|
5
|
+
# Check or install the app dependencies via Bundler:
|
6
|
+
bundle check || bundle
|
7
|
+
|
8
|
+
# Specify a default command, in case it wasn't issued:
|
9
|
+
if [ -z "$1" ]; then
|
10
|
+
set -- bundle exec rails s -b 0.0.0.0 -p 3000 "$@"
|
11
|
+
fi
|
12
|
+
|
13
|
+
# Execute the given or default command:
|
14
|
+
exec "$@"
|
15
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
RSpec.configure do |config|
|
4
|
+
config.root_dir = Rails.root.join('open-api').to_s
|
5
|
+
config.open_api_docs = {
|
6
|
+
'v1/openapi.json' => {
|
7
|
+
openapi: '3.0.0',
|
8
|
+
info: {
|
9
|
+
title: 'API V1',
|
10
|
+
version: 'v1'
|
11
|
+
},
|
12
|
+
paths: {},
|
13
|
+
servers: [
|
14
|
+
{
|
15
|
+
url: 'https://{defaultHost}',
|
16
|
+
variables: {
|
17
|
+
defaultHost: {
|
18
|
+
default: 'www.example.com'
|
19
|
+
}
|
20
|
+
}
|
21
|
+
}
|
22
|
+
]
|
23
|
+
}
|
24
|
+
}
|
25
|
+
end
|
data/lib/ropen_pi.rb
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
# helper for the open api documentation
|
2
|
+
module RopenPi
|
3
|
+
module Param
|
4
|
+
#
|
5
|
+
def self.date_param(name, desc: 'tba', opts: {})
|
6
|
+
param(name, :string, fmt: 'date-time', desc: desc, opts: opts)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.uuid_param(name, desc: 'tba', opts: {})
|
10
|
+
param(name, :string, fmt: 'uuid', desc: desc, opts: opts)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.string_param(name, desc: 'tba', opts: {})
|
14
|
+
param(name, :string, desc: desc, opts: opts)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.int_param(name, desc: 'tba', opts: {})
|
18
|
+
param(name, :integer, desc: desc, opts: opts)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.bool_param(name, desc: 'tba', opts: {})
|
22
|
+
param(name, :boolean, desc: desc, opts: opts)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.ref_param(name, ref, desc: 'tba', opts: {})
|
26
|
+
param(name, 'null', desc: desc, opts: opts).merge(schema: { '$ref': ref })
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.param_in_path(name, schema_type, fmt: nil, desc: 'tba', opts: {})
|
30
|
+
param(name, schema_type, fmt: fmt, desc: desc, opts: opts)
|
31
|
+
.merge(
|
32
|
+
in: 'path',
|
33
|
+
required: true
|
34
|
+
)
|
35
|
+
.merge(opts)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.param(name, schema_type, fmt: nil, desc: 'tba', opts: {})
|
39
|
+
{
|
40
|
+
name: name.to_s,
|
41
|
+
description: desc,
|
42
|
+
in: 'query',
|
43
|
+
schema: schema(schema_type, fmt: fmt)
|
44
|
+
}.merge(opts)
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.schema(type, fmt: nil)
|
48
|
+
{}.tap do |schema|
|
49
|
+
schema[:type] = type.to_s
|
50
|
+
schema[:format] = fmt.to_s if fmt.present?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.schema_enum(values, type: :string)
|
55
|
+
{
|
56
|
+
type: type.to_s,
|
57
|
+
enum: values
|
58
|
+
}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
module Type
|
63
|
+
def self.date_time_type(opts = {})
|
64
|
+
string_type(opts).merge(format: 'date-time', example: '2020-02-02')
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.email_type(opts = {})
|
68
|
+
string_type(opts).merge(format: 'email', example: 'han.solo@example.com')
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.uuid_type(opts = {})
|
72
|
+
string_type(opts).merge(format: 'uuid', example: 'abcd12-1234ab-abcdef123')
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.string_type(opts = {})
|
76
|
+
type('string', opts).merge(example: 'Example string')
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.integer_type(opts = {})
|
80
|
+
type('integer', opts).merge(example: 1)
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.bool_type(opts = {})
|
84
|
+
type('boolean', opts).merge(example: true)
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.type(thing, opts = {})
|
88
|
+
{ type: thing }.merge(opts)
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.string_array_type(opts = {})
|
92
|
+
{
|
93
|
+
type: 'array',
|
94
|
+
items: { type: 'string', example: 'Example string' }
|
95
|
+
}.merge(opts)
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.ref_type(ref)
|
99
|
+
{ '$ref': ref }
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
module Response
|
104
|
+
# rubocop:disable Metrics/MethodLength
|
105
|
+
def self.collection(ref, desc: 'tba', type: RopenPi::APP_JSON)
|
106
|
+
{
|
107
|
+
description: desc,
|
108
|
+
content: {
|
109
|
+
type => {
|
110
|
+
schema: {
|
111
|
+
type: 'object',
|
112
|
+
properties: { data: { type: :array, items: { '$ref': ref } } }
|
113
|
+
}
|
114
|
+
}
|
115
|
+
}
|
116
|
+
}
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.single(ref, desc: 'tba', type: RopenPi::APP_JSON)
|
120
|
+
{
|
121
|
+
description: desc,
|
122
|
+
content: {
|
123
|
+
type => {
|
124
|
+
schema: {
|
125
|
+
type: 'object',
|
126
|
+
properties: { data: { '$ref': ref } }
|
127
|
+
}
|
128
|
+
}
|
129
|
+
}
|
130
|
+
}
|
131
|
+
end
|
132
|
+
# rubocop:enable Metrics/MethodLength
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rspec/core'
|
2
|
+
require 'ropen_pi/specs/example_group_helpers'
|
3
|
+
require 'ropen_pi/specs/example_helpers'
|
4
|
+
require 'ropen_pi/specs/configuration'
|
5
|
+
require 'ropen_pi/specs/railtie' if defined?(Rails::Railtie)
|
6
|
+
|
7
|
+
module RopenPi
|
8
|
+
module Specs
|
9
|
+
# Extend RSpec with a swagger-based DSL
|
10
|
+
::RSpec.configure do |config|
|
11
|
+
config.add_setting :root_dir
|
12
|
+
config.add_setting :open_api_docs
|
13
|
+
config.add_setting :open_api_output_format
|
14
|
+
|
15
|
+
config.extend ExampleGroupHelpers, type: :request
|
16
|
+
config.include ExampleHelpers, type: :request
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.config
|
20
|
+
@config ||= Configuration.new(RSpec.configuration)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class RopenPi::Specs::Configuration
|
4
|
+
def initialize(rspec_config)
|
5
|
+
@rspec_config = rspec_config
|
6
|
+
end
|
7
|
+
|
8
|
+
def root_dir
|
9
|
+
@root_dir ||= begin
|
10
|
+
raise ConfigurationError, 'No root_dir provided. See open_api_helper.rb' if @rspec_config.root_dir.nil?
|
11
|
+
|
12
|
+
@rspec_config.root_dir
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def open_api_output_format
|
17
|
+
@open_api_output_format ||= begin
|
18
|
+
@rspec_config.open_api_output_format || :json
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def open_api_docs
|
23
|
+
@open_api_docs ||= begin
|
24
|
+
if @rspec_config.open_api_docs.nil? || @rspec_config.open_api_docs.empty?
|
25
|
+
raise ConfigurationError, 'No open_api_docs defined. See open_api_helper.rb'
|
26
|
+
end
|
27
|
+
|
28
|
+
@rspec_config.open_api_docs
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def get_doc(name)
|
33
|
+
return open_api_docs.values.first if name.nil?
|
34
|
+
|
35
|
+
raise ConfigurationError, "Unknown doc '#{name}'" unless open_api_docs[name]
|
36
|
+
|
37
|
+
open_api_docs[name]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class ConfigurationError < StandardError; end
|
@@ -0,0 +1,284 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# @TODO: replace hashie!
|
3
|
+
require 'hashie'
|
4
|
+
|
5
|
+
module RopenPi
|
6
|
+
module Specs
|
7
|
+
# rubocop:disable Metrics/ModuleLength
|
8
|
+
module ExampleGroupHelpers
|
9
|
+
def path(template, metadata = {}, &block)
|
10
|
+
metadata[:path_item] = { template: template }
|
11
|
+
describe(template, metadata, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
%i[get post patch put delete head].each do |verb|
|
15
|
+
define_method(verb) do |summary, &block|
|
16
|
+
api_metadata = { operation: { verb: verb, summary: summary } }
|
17
|
+
describe(verb, api_metadata, &block)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
%i[operationId deprecated security].each do |attr_name|
|
22
|
+
define_method(attr_name) do |value|
|
23
|
+
metadata[:operation][attr_name] = value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# NOTE: 'description' requires special treatment because ExampleGroup already
|
28
|
+
# defines a method with that name. Provide an override that supports the existing
|
29
|
+
# functionality while also setting the appropriate metadata if applicable
|
30
|
+
def description(value = nil)
|
31
|
+
return super() if value.nil?
|
32
|
+
|
33
|
+
metadata[:operation][:description] = value
|
34
|
+
end
|
35
|
+
|
36
|
+
# These are array properties - note the splat operator
|
37
|
+
%i[tags consumes produces schemes].each do |attr_name|
|
38
|
+
define_method(attr_name) do |*value|
|
39
|
+
metadata[:operation][attr_name] = value
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def request_body(attributes)
|
44
|
+
# can make this generic, and accept any incoming hash (like parameter method)
|
45
|
+
attributes.compact!
|
46
|
+
|
47
|
+
if metadata[:operation][:requestBody].blank?
|
48
|
+
metadata[:operation][:requestBody] = attributes
|
49
|
+
elsif metadata[:operation][:requestBody] && metadata[:operation][:requestBody][:content]
|
50
|
+
# merge in
|
51
|
+
content_hash = metadata[:operation][:requestBody][:content]
|
52
|
+
incoming_content_hash = attributes[:content]
|
53
|
+
content_hash.merge!(incoming_content_hash) if incoming_content_hash
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def request_body_json(schema:, required: true, description: nil, examples: nil)
|
58
|
+
passed_examples = Array(examples)
|
59
|
+
content_hash = { 'application/json' => { schema: schema, examples: examples }.compact! || {} }
|
60
|
+
request_body(description: description, required: required, content: content_hash)
|
61
|
+
|
62
|
+
handle_examples(schema: schema, examples: passed_examples, required: required) if passed_examples.any?
|
63
|
+
end
|
64
|
+
|
65
|
+
# rubocop:disable Metrics/MethodLength
|
66
|
+
def handle_examples(schema:, examples:, required:)
|
67
|
+
# the request_factory is going to have to resolve the different ways that the example can be given
|
68
|
+
# it can contain a 'value' key which is a direct hash (easiest)
|
69
|
+
# it can contain a 'external_value' key which makes an external call to load the json
|
70
|
+
# it can contain a '$ref' key. Which points to #/components/examples/blog
|
71
|
+
# rubocop:disable Metrics/BlockLength
|
72
|
+
examples.each do |passed_example|
|
73
|
+
param_attributes = if passed_example.is_a?(Symbol)
|
74
|
+
example_key_name = passed_example
|
75
|
+
# TODO: write more tests around this adding to the parameter
|
76
|
+
# if symbol try and use save_request_example
|
77
|
+
{
|
78
|
+
name: example_key_name,
|
79
|
+
in: :body,
|
80
|
+
required: required,
|
81
|
+
param_value: example_key_name,
|
82
|
+
schema: schema
|
83
|
+
}
|
84
|
+
elsif passed_example.is_a?(Hash) && passed_example[:externalValue]
|
85
|
+
{
|
86
|
+
name: passed_example,
|
87
|
+
in: :body,
|
88
|
+
required: required,
|
89
|
+
param_value: passed_example[:externalValue],
|
90
|
+
schema: schema
|
91
|
+
}
|
92
|
+
elsif passed_example.is_a?(Hash) && passed_example['$ref']
|
93
|
+
{
|
94
|
+
name: passed_example,
|
95
|
+
in: :body,
|
96
|
+
required: required,
|
97
|
+
param_value: passed_example['$ref'],
|
98
|
+
schema: schema
|
99
|
+
}
|
100
|
+
end
|
101
|
+
parameter(param_attributes)
|
102
|
+
end
|
103
|
+
# rubocop:enable Metrics/BlockLength
|
104
|
+
end
|
105
|
+
# rubocop:enable Metrics/MethodLength
|
106
|
+
|
107
|
+
def request_body_text_plain(required: false, description: nil, examples: nil)
|
108
|
+
content_hash = { 'test/plain' => { schema: {type: :string}, examples: examples }.compact! || {} }
|
109
|
+
request_body(description: description, required: required, content: content_hash)
|
110
|
+
end
|
111
|
+
|
112
|
+
# TODO: add examples to this like we can for json, might be large lift
|
113
|
+
# as many assumptions are made on content-type
|
114
|
+
def request_body_xml(schema:, required: false, description: nil, examples: nil)
|
115
|
+
passed_examples = Array(examples)
|
116
|
+
content_hash = { 'application/xml' => { schema: schema, examples: examples }.compact! || {} }
|
117
|
+
request_body(description: description, required: required, content: content_hash)
|
118
|
+
end
|
119
|
+
|
120
|
+
def request_body_multipart(schema:, description: nil)
|
121
|
+
content_hash = { 'multipart/form-data' => { schema: schema } }
|
122
|
+
request_body(description: description, content: content_hash)
|
123
|
+
|
124
|
+
schema.extend(Hashie::Extensions::DeepLocate)
|
125
|
+
file_properties = schema.deep_locate ->(_k, v, _obj) { v == :binary }
|
126
|
+
hash_locator = []
|
127
|
+
|
128
|
+
file_properties.each do |match|
|
129
|
+
hash_match = schema.deep_locate ->(_k, v, _obj) { v == match }
|
130
|
+
hash_locator.concat(hash_match) unless hash_match.empty?
|
131
|
+
end
|
132
|
+
|
133
|
+
property_hashes = hash_locator.flat_map do |locator|
|
134
|
+
locator.select { |_k, v| file_properties.include?(v) }
|
135
|
+
end
|
136
|
+
|
137
|
+
existing_keys = []
|
138
|
+
property_hashes.each do |property_hash|
|
139
|
+
property_hash.keys.each do |k|
|
140
|
+
next if existing_keys.include?(k)
|
141
|
+
|
142
|
+
file_name = k
|
143
|
+
existing_keys << k
|
144
|
+
parameter name: file_name, in: :formData, type: :file, required: true
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def parameter(attributes)
|
150
|
+
attributes[:required] = true if attributes[:in] && attributes[:in].to_sym == :path
|
151
|
+
attributes[:schema] = { type: attributes[:type] } if attributes[:type] && attributes[:schema].nil?
|
152
|
+
|
153
|
+
if metadata.key?(:operation)
|
154
|
+
metadata[:operation][:parameters] ||= []
|
155
|
+
metadata[:operation][:parameters] << attributes
|
156
|
+
else
|
157
|
+
metadata[:path_item][:parameters] ||= []
|
158
|
+
metadata[:path_item][:parameters] << attributes
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def response(code, description, metadata = {}, &block)
|
163
|
+
metadata[:response] = { code: code, description: description }
|
164
|
+
context(description, metadata, &block)
|
165
|
+
end
|
166
|
+
|
167
|
+
def schema(value, content_type: 'application/json')
|
168
|
+
content_hash = { content_type => { schema: value } }
|
169
|
+
metadata[:response][:content] = content_hash
|
170
|
+
end
|
171
|
+
|
172
|
+
def header(name, attributes)
|
173
|
+
metadata[:response][:headers] ||= {}
|
174
|
+
|
175
|
+
if attributes[:type] && attributes[:schema].nil?
|
176
|
+
attributes[:schema] = { type: attributes[:type] }
|
177
|
+
attributes.delete(:type)
|
178
|
+
end
|
179
|
+
|
180
|
+
metadata[:response][:headers][name] = attributes
|
181
|
+
end
|
182
|
+
|
183
|
+
# NOTE: Similar to 'description', 'examples' need to handle the case when
|
184
|
+
# being invoked with no params to avoid overriding 'examples' method of
|
185
|
+
# rspec-core ExampleGroup
|
186
|
+
def examples(example = nil)
|
187
|
+
return super() if example.nil?
|
188
|
+
|
189
|
+
metadata[:response][:examples] = example
|
190
|
+
end
|
191
|
+
|
192
|
+
# checks the examples in the parameters should be able to add $ref and externalValue examples.
|
193
|
+
# This syntax would look something like this in the integration _spec.rb file
|
194
|
+
#
|
195
|
+
# request_body_json schema: { '$ref' => '#/components/schemas/blog' },
|
196
|
+
# examples: [:blog, {name: :external_blog,
|
197
|
+
# externalValue: 'http://api.sample.org/myjson_example'},
|
198
|
+
# {name: :another_example,
|
199
|
+
# '$ref' => '#/components/examples/flexible_blog_example'}]
|
200
|
+
# The first value :blog, points to a let param of the same name, and is used to make the request in the
|
201
|
+
# integration test (it is used to build the request payload)
|
202
|
+
#
|
203
|
+
# The second item in the array shows how to add an externalValue for the examples in the requestBody section
|
204
|
+
# The third item shows how to add a $ref item that points to the components/examples section of the swagger spec.
|
205
|
+
#
|
206
|
+
# NOTE: that the externalValue will produce valid example syntax in the swagger output, but swagger-ui
|
207
|
+
# will not show it yet
|
208
|
+
def merge_other_examples!(example_metadata)
|
209
|
+
# example.metadata[:operation][:requestBody][:content]['application/json'][:examples]
|
210
|
+
content_node = example_metadata[:operation][:requestBody][:content]['application/json']
|
211
|
+
return unless content_node
|
212
|
+
|
213
|
+
external_example = example_metadata[:operation]&.dig(:parameters)&.detect do |p|
|
214
|
+
p[:in] == :body && p[:name].is_a?(Hash) && p[:name][:externalValue]
|
215
|
+
end || {}
|
216
|
+
|
217
|
+
ref_example = example_metadata[:operation]&.dig(:parameters)&.detect do |p|
|
218
|
+
p[:in] == :body && p[:name].is_a?(Hash) && p[:name]['$ref']
|
219
|
+
end || {}
|
220
|
+
|
221
|
+
examples_node = content_node[:examples] ||= {}
|
222
|
+
|
223
|
+
nodes_to_add = []
|
224
|
+
nodes_to_add << external_example unless external_example.empty?
|
225
|
+
nodes_to_add << ref_example unless ref_example.empty?
|
226
|
+
|
227
|
+
nodes_to_add.each do |node|
|
228
|
+
json_request_examples = examples_node ||= {}
|
229
|
+
other_name = node[:name][:name]
|
230
|
+
other_key = node[:name][:externalValue] ? :externalValue : '$ref'
|
231
|
+
|
232
|
+
json_request_examples.merge!(other_name => { other_key => node[:param_value] }) if other_name
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def run_test!(&block)
|
237
|
+
app_json = 'application/json'
|
238
|
+
|
239
|
+
before do |example|
|
240
|
+
submit_request(example.metadata)
|
241
|
+
end
|
242
|
+
|
243
|
+
it "returns a #{metadata[:response][:code]} response" do |example|
|
244
|
+
assert_response_matches_metadata(example.metadata, &block)
|
245
|
+
example.instance_exec(response, &block) if block_given?
|
246
|
+
end
|
247
|
+
|
248
|
+
after do |example|
|
249
|
+
body_parameter = example.metadata[:operation]&.dig(:parameters)&.detect do |p|
|
250
|
+
p[:in] == :body && p[:required]
|
251
|
+
end
|
252
|
+
|
253
|
+
if body_parameter && respond_to?(body_parameter[:name]) && \
|
254
|
+
example.metadata[:operation][:requestBody][:content][app_json]
|
255
|
+
|
256
|
+
# save response examples by default
|
257
|
+
if example.metadata[:response][:examples].nil? || example.metadata[:response][:examples].empty?
|
258
|
+
unless response.body.to_s.empty?
|
259
|
+
example.metadata[:response][:examples] = {
|
260
|
+
app_json => JSON.parse(response.body, symbolize_names: true)
|
261
|
+
}
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# save request examples using the let(:param_name) { REQUEST_BODY_HASH } syntax in the test
|
266
|
+
if response.code.to_s =~ /^2\d{2}$/
|
267
|
+
|
268
|
+
example.metadata[:operation][:requestBody][:content][app_json] = { examples: {} } \
|
269
|
+
unless example.metadata[:operation][:requestBody][:content][app_json][:examples]
|
270
|
+
|
271
|
+
json_request_examples = example.metadata[:operation][:requestBody][:content][app_json][:examples]
|
272
|
+
json_request_examples[body_parameter[:name]] = { value: send(body_parameter[:name]) }
|
273
|
+
|
274
|
+
example.metadata[:operation][:requestBody][:content][app_json][:examples] = json_request_examples
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
self.class.merge_other_examples!(example.metadata) if example.metadata[:operation][:requestBody]
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
# rubocop:enable Metrics/ModuleLength
|
283
|
+
end
|
284
|
+
end
|