ropen_pi 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +12 -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 +144 -0
- data/lib/ropen_pi/specs.rb +23 -0
- data/lib/ropen_pi/specs/configuration.rb +45 -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,12 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
module Generators
|
3
|
+
module RopenPi
|
4
|
+
class InstallGenerator < Rails::Generators::Base
|
5
|
+
source_root File.expand_path('templates', __dir__)
|
6
|
+
|
7
|
+
def add_open_api_helper
|
8
|
+
template('ropen_pi_helper.rb', 'spec/ropen_pi_helper.rb')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -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,144 @@
|
|
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
|
+
|
61
|
+
class << self
|
62
|
+
alias boolean_param bool_param
|
63
|
+
alias integer_param int_param
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
module Type
|
68
|
+
def self.date_time_type(opts = { example: '2020-02-02' })
|
69
|
+
string_type(opts).merge(format: 'date-time')
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.email_type(opts = { example: 'han.solo@example.com' })
|
73
|
+
string_type(opts).merge(format: 'email')
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.uuid_type(opts = { example: 'abcd12-1234ab-abcdef123' })
|
77
|
+
string_type(opts).merge(format: 'uuid')
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.string_type(opts = { example: 'Example string' })
|
81
|
+
type('string', opts)
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.integer_type(opts = { example: 1 })
|
85
|
+
type('integer', opts)
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.bool_type(opts = { example: true })
|
89
|
+
type('boolean', opts)
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.type(thing, opts = {})
|
93
|
+
{ type: thing }.merge(opts)
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.string_array_type(opts = {})
|
97
|
+
{
|
98
|
+
type: 'array',
|
99
|
+
items: { type: 'string', example: 'Example string' }
|
100
|
+
}.merge(opts)
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.ref_type(ref)
|
104
|
+
{ '$ref': ref }
|
105
|
+
end
|
106
|
+
|
107
|
+
class << self
|
108
|
+
alias boolean_type bool_type
|
109
|
+
alias int_type integer_type
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
module Response
|
114
|
+
# rubocop:disable Metrics/MethodLength
|
115
|
+
def self.collection(ref, desc: 'tba', type: RopenPi::APP_JSON)
|
116
|
+
{
|
117
|
+
description: desc,
|
118
|
+
content: {
|
119
|
+
type => {
|
120
|
+
schema: {
|
121
|
+
type: 'object',
|
122
|
+
properties: { data: { type: :array, items: { '$ref': ref } } }
|
123
|
+
}
|
124
|
+
}
|
125
|
+
}
|
126
|
+
}
|
127
|
+
end
|
128
|
+
|
129
|
+
def self.single(ref, desc: 'tba', type: RopenPi::APP_JSON)
|
130
|
+
{
|
131
|
+
description: desc,
|
132
|
+
content: {
|
133
|
+
type => {
|
134
|
+
schema: {
|
135
|
+
type: 'object',
|
136
|
+
properties: { data: { '$ref': ref } }
|
137
|
+
}
|
138
|
+
}
|
139
|
+
}
|
140
|
+
}
|
141
|
+
end
|
142
|
+
# rubocop:enable Metrics/MethodLength
|
143
|
+
end
|
144
|
+
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,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module RopenPi
|
3
|
+
module Specs
|
4
|
+
class Configuration
|
5
|
+
def initialize(rspec_config)
|
6
|
+
@rspec_config = rspec_config
|
7
|
+
end
|
8
|
+
|
9
|
+
def root_dir
|
10
|
+
@root_dir ||= begin
|
11
|
+
raise ConfigurationError, 'No root_dir provided. See open_api_helper.rb' if @rspec_config.root_dir.nil?
|
12
|
+
|
13
|
+
@rspec_config.root_dir
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def open_api_output_format
|
18
|
+
@open_api_output_format ||= begin
|
19
|
+
@rspec_config.open_api_output_format || :json
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def open_api_docs
|
24
|
+
@open_api_docs ||= begin
|
25
|
+
if @rspec_config.open_api_docs.nil? || @rspec_config.open_api_docs.empty?
|
26
|
+
raise ConfigurationError, 'No open_api_docs defined. See open_api_helper.rb'
|
27
|
+
end
|
28
|
+
|
29
|
+
@rspec_config.open_api_docs
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def get_doc(name)
|
34
|
+
return open_api_docs.values.first if name.nil?
|
35
|
+
|
36
|
+
raise ConfigurationError, "Unknown doc '#{name}'" unless open_api_docs[name]
|
37
|
+
|
38
|
+
open_api_docs[name]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class ConfigurationError < StandardError; end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
@@ -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
|