swagalicious 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 941a558739b2079027c117ccea67ffc90c7a21f682811ab817a739ef0ea93d82
4
+ data.tar.gz: 275cce80dedbfcaa5b5e76d01499fe18babcc7e30a948806ad95df1cea46d475
5
+ SHA512:
6
+ metadata.gz: 2e8f90a288217b8e27e85411194a73a7f5d1bbe7bd8881a1f086c11319b8ee9a832143bc9c610a8398fb11051484be70f828f3a1625d5c2180d5e1d1194e5121
7
+ data.tar.gz: 181ff6d1a9dcab5b13499f648b75299f4ab6321c2ea4d65956a94d4bd8b62f90d8e049c771629ed457ca828635f7f90ab35f9bba630b437d0ebb11136d0cdb19
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,74 @@
1
+ require: rubocop-rspec
2
+
3
+ AllCops:
4
+ DisplayCopNames: true
5
+ DisabledByDefault: true
6
+ Exclude:
7
+ - "cypress/**/*"
8
+ - "db/migrations/**/*.rb"
9
+ - "docs/**/*"
10
+ - "node_modules/**/*"
11
+ - "private/**/*"
12
+ - "public/**/*"
13
+ - "static/**/*"
14
+ - "tmp/**/*"
15
+ - "vendor/**/*"
16
+
17
+ Layout/EmptyLines:
18
+ Enabled: true
19
+
20
+ Layout/ExtraSpacing:
21
+ Enabled: true
22
+ ForceEqualSignAlignment: true
23
+
24
+ Layout/HashAlignment:
25
+ Enabled: true
26
+ EnforcedColonStyle: table
27
+ EnforcedHashRocketStyle: table
28
+ EnforcedLastArgumentHashStyle: ignore_implicit
29
+
30
+ Layout/TrailingEmptyLines:
31
+ Enabled: true
32
+
33
+ Layout/TrailingWhitespace:
34
+ Enabled: true
35
+
36
+ RSpec/AlignLeftLetBrace:
37
+ Enabled: true
38
+
39
+ RSpec/DescribedClass:
40
+ EnforcedStyle: described_class
41
+
42
+ RSpec/EmptyLineAfterFinalLet:
43
+ Enabled: true
44
+
45
+ Style/Alias:
46
+ EnforcedStyle: prefer_alias_method
47
+
48
+ Style/Documentation:
49
+ Enabled: false
50
+
51
+ Style/HashEachMethods:
52
+ Enabled: false
53
+
54
+ Style/HashSyntax:
55
+ Enabled: true
56
+ EnforcedStyle: ruby19
57
+
58
+ Style/HashTransformKeys:
59
+ Enabled: true
60
+
61
+ Style/HashTransformValues:
62
+ Enabled: false
63
+
64
+ Style/Lambda:
65
+ EnforcedStyle: literal
66
+
67
+ Style/SignalException:
68
+ Enabled: false
69
+
70
+ Style/StringLiterals:
71
+ EnforcedStyle: double_quotes
72
+
73
+ Style/StringLiteralsInInterpolation:
74
+ EnforcedStyle: double_quotes
@@ -0,0 +1,6 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.6.5
6
+ before_install: gem install bundler -v 2.1.2
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in swagalicious.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 12.0"
7
+ gem "rspec", "~> 3.0"
@@ -0,0 +1,50 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ swagalicious (0.1.0)
5
+ faraday (~> 1.0.1)
6
+ json-schema (~> 2.8.1)
7
+ oj (~> 3.10.14)
8
+ rack-test (~> 1.1.0)
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ addressable (2.7.0)
14
+ public_suffix (>= 2.0.2, < 5.0)
15
+ diff-lcs (1.4.4)
16
+ faraday (1.0.1)
17
+ multipart-post (>= 1.2, < 3)
18
+ json-schema (2.8.1)
19
+ addressable (>= 2.4)
20
+ multipart-post (2.1.1)
21
+ oj (3.10.14)
22
+ public_suffix (4.0.6)
23
+ rack (2.2.3)
24
+ rack-test (1.1.0)
25
+ rack (>= 1.0, < 3)
26
+ rake (12.3.3)
27
+ rspec (3.9.0)
28
+ rspec-core (~> 3.9.0)
29
+ rspec-expectations (~> 3.9.0)
30
+ rspec-mocks (~> 3.9.0)
31
+ rspec-core (3.9.2)
32
+ rspec-support (~> 3.9.3)
33
+ rspec-expectations (3.9.2)
34
+ diff-lcs (>= 1.2.0, < 2.0)
35
+ rspec-support (~> 3.9.0)
36
+ rspec-mocks (3.9.1)
37
+ diff-lcs (>= 1.2.0, < 2.0)
38
+ rspec-support (~> 3.9.0)
39
+ rspec-support (3.9.3)
40
+
41
+ PLATFORMS
42
+ ruby
43
+
44
+ DEPENDENCIES
45
+ rake (~> 12.0)
46
+ rspec (~> 3.0)
47
+ swagalicious!
48
+
49
+ BUNDLED WITH
50
+ 2.1.2
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Eugene Howe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,65 @@
1
+ # Swagalicious
2
+
3
+ This gem is an implementation of https://github.com/rswag/rswag/blob/master/rswag-specs that does not rely on Rails. Most of the code is a blatant copy/paste from that repo, most of the credit goes to them.
4
+
5
+ Currenty it does not implement any API or UI. In the application that is using this gem, we are using https://github.com/Redocly/redoc that is accessed through a rack middleware.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'swagalicious'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle install
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install swagalicious
22
+
23
+ ## Usage
24
+
25
+ Add the following to your `spec_helper.rb` or add a new `swagger_helper.rb`
26
+
27
+ ```ruby
28
+ require 'swagalicious`
29
+
30
+ DEFINITIONS = Oj.load(File.read(File.expand_path("docs/definitions.json", __dir__))).freeze
31
+
32
+ RSpec.configure do |c|
33
+ c.swagger_root = "public/swagger_docs" # This is the relative path where the swagger docs will be output
34
+ c.swagger_docs = {
35
+ "path/to/swagger_doc.json" => {
36
+ swagger: "3.0",
37
+ basePath: "/api/",
38
+ version: "v1",
39
+ info: {
40
+ title: "Namespace for my API"
41
+ },
42
+ components: {
43
+ securitySchemes: {
44
+ apiKey: {
45
+ type: :apiKey,
46
+ name: "authorization",
47
+ in: :header,
48
+ }
49
+ }
50
+ },
51
+ }
52
+ }
53
+ end
54
+ ```
55
+
56
+
57
+
58
+ ## Contributing
59
+
60
+ Bug reports and pull requests are welcome on GitHub at https://github.com/eugene@xtreme-computers.net/swagalicious.
61
+
62
+
63
+ ## License
64
+
65
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "swagalicious"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,19 @@
1
+ class Hash
2
+ def deep_merge!(other_hash)
3
+ self.merge(other_hash) do |key, oldval, newval|
4
+ oldval = oldval.to_hash if oldval.respond_to?(:to_hash)
5
+ newval = newval.to_hash if newval.respond_to?(:to_hash)
6
+ oldval.class.to_s == "Hash" && newval.class.to_s == "Hash" ? oldval.deep_merge(newval) : newval
7
+ end
8
+ end
9
+
10
+ def slice(*keep_keys)
11
+ h = {}
12
+ keep_keys.each { |key| has_key?(key) && h[key] = fetch(key, nil) }
13
+ h
14
+ end
15
+
16
+ def except(*less_keys)
17
+ slice(*keys - less_keys)
18
+ end
19
+ end
@@ -0,0 +1,30 @@
1
+ require "rspec/core"
2
+
3
+ require_relative "swagalicious/version"
4
+
5
+ module Swagalicious
6
+ class Error < StandardError; end
7
+
8
+ require_relative "swagalicious/configuration"
9
+ require_relative "swagalicious/example_group_helpers"
10
+ require_relative "swagalicious/example_helpers"
11
+ require_relative "swagalicious/extended_schema"
12
+ require_relative "swagalicious/request_factory"
13
+ require_relative "swagalicious/response_validator"
14
+ require_relative "swagalicious/swagger_formatter"
15
+
16
+ ::RSpec::Core::ExampleGroup.define_example_group_method :path
17
+
18
+ ::RSpec.configure do |c|
19
+ c.add_setting :swagger_format
20
+ c.add_setting :swagger_root
21
+ c.add_setting :swagger_docs
22
+ c.add_setting :swagger_dry_run
23
+ c.extend Swagalicious::ExampleGroupHelpers, type: :doc
24
+ c.include Swagalicious::ExampleHelpers, type: :doc
25
+ end
26
+
27
+ def self.config
28
+ @config ||= Swagalicious::Configuration.new(RSpec.configuration)
29
+ end
30
+ end
@@ -0,0 +1,53 @@
1
+ module Swagalicious
2
+ class ConfigurationError < StandardError; end
3
+
4
+ class Configuration
5
+ def initialize(rspec_config)
6
+ @rspec_config = rspec_config
7
+ end
8
+
9
+ def swagger_root
10
+ @swagger_root ||= begin
11
+ if @rspec_config.swagger_root.nil?
12
+ raise ConfigurationError, "No swagger_root provided. See swagger_helper.rb"
13
+ end
14
+ @rspec_config.swagger_root
15
+ end
16
+ end
17
+
18
+ def swagger_docs
19
+ @swagger_docs ||= begin
20
+ if @rspec_config.swagger_docs.nil? || @rspec_config.swagger_docs.empty?
21
+ raise ConfigurationError, "No swagger_docs defined. See swagger_helper.rb"
22
+ end
23
+ @rspec_config.swagger_docs
24
+ end
25
+ end
26
+
27
+ def swagger_dry_run
28
+ @swagger_dry_run ||= begin
29
+ @rspec_config.swagger_dry_run.nil? || @rspec_config.swagger_dry_run
30
+ end
31
+ end
32
+
33
+ def swagger_format
34
+ @swagger_format ||= begin
35
+ @rspec_config.swagger_format = :json if @rspec_config.swagger_format.nil? || @rspec_config.swagger_format.empty?
36
+ raise ConfigurationError, "Unknown swagger_format '#{@rspec_config.swagger_format}'" unless [:json, :yaml].include?(@rspec_config.swagger_format)
37
+ @rspec_config.swagger_format
38
+ end
39
+ end
40
+
41
+ def get_swagger_doc(name)
42
+ return swagger_docs.values.first if name.nil?
43
+ raise ConfigurationError, "Unknown swagger_doc '#{name}'" unless swagger_docs[name]
44
+
45
+ swagger_docs[name]
46
+ end
47
+
48
+ def get_swagger_doc_version(name)
49
+ doc = get_swagger_doc(name)
50
+ doc[:openapi] || doc[:swagger]
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,79 @@
1
+ module Swagalicious
2
+ module ExampleGroupHelpers
3
+ def path(template, metadata={}, &block)
4
+ metadata[:path_item] = { template: template }
5
+ describe(template, metadata, &block)
6
+ end
7
+
8
+ [ :get, :post, :patch, :put, :delete, :head ].each do |verb|
9
+ define_method(verb) do |summary, &block|
10
+ api_metadata = { operation: { verb: verb, summary: summary } }
11
+ describe(verb, api_metadata, &block)
12
+ end
13
+ end
14
+
15
+ [ :operationId, :deprecated, :security ].each do |attr_name|
16
+ define_method(attr_name) do |value|
17
+ metadata[:operation][attr_name] = value
18
+ end
19
+ end
20
+
21
+ # NOTE: 'description' requires special treatment because ExampleGroup already
22
+ # defines a method with that name. Provide an override that supports the existing
23
+ # functionality while also setting the appropriate metadata if applicable
24
+ def description(value=nil)
25
+ return super() if value.nil?
26
+ metadata[:operation][:description] = value
27
+ end
28
+
29
+ # These are array properties - note the splat operator
30
+ [ :tags, :consumes, :produces, :schemes ].each do |attr_name|
31
+ define_method(attr_name) do |*value|
32
+ metadata[:operation][attr_name] = value
33
+ end
34
+ end
35
+
36
+ def parameter(attributes)
37
+ if attributes[:in] && attributes[:in].to_sym == :path
38
+ attributes[:required] = true
39
+ end
40
+
41
+ if metadata.has_key?(:operation)
42
+ metadata[:operation][:parameters] ||= []
43
+ metadata[:operation][:parameters] << attributes
44
+ else
45
+ metadata[:path_item][:parameters] ||= []
46
+ metadata[:path_item][:parameters] << attributes
47
+ end
48
+ end
49
+
50
+ def response(code, description, metadata={}, &block)
51
+ metadata[:response] = { code: code, description: description }
52
+ context(description, metadata, &block)
53
+ end
54
+
55
+ def schema(value)
56
+ metadata[:response][:schema] = value
57
+ end
58
+
59
+ def header(name, attributes)
60
+ metadata[:response][:headers] ||= {}
61
+ metadata[:response][:headers][name] = attributes
62
+ end
63
+
64
+ # NOTE: Similar to 'description', 'examples' need to handle the case when
65
+ # being invoked with no params to avoid overriding 'examples' method of
66
+ # rspec-core ExampleGroup
67
+ def examples(example = nil)
68
+ return super() if example.nil?
69
+ metadata[:response][:examples] = example
70
+ end
71
+
72
+ def validate_schema!(mocked: false, mock_name: nil)
73
+ it "returns a #{metadata[:response][:code]} response" do |example|
74
+ yield if block_given?
75
+ submit_request(example.metadata, mocked: mocked, mock_name: mock_name)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,65 @@
1
+ require "faraday"
2
+ require "faraday/adapter/rack"
3
+ require "rack"
4
+ require "oj"
5
+
6
+ require_relative "response_validator"
7
+
8
+ module Swagalicious
9
+ module ExampleHelpers
10
+ include Rack::Test::Methods
11
+
12
+ class MockResponse
13
+ attr_reader :body, :status, :headers
14
+
15
+ def initialize(file_name)
16
+ @body = File.read(File.expand_path("#{File.join(ENV["MOCK_PATH"], file_name)}.json", __FILE__))
17
+ @status = 200
18
+ @headers = {}
19
+ end
20
+ end
21
+
22
+ def app
23
+ @app ||= Rack::Builder.parse_file("config.ru").first
24
+ end
25
+
26
+ def client
27
+ @client ||= Faraday.new do |b|
28
+ b.adapter Faraday::Adapter::Rack, app
29
+ end
30
+ end
31
+
32
+ def submit_request(metadata, mocked: false, mock_name: nil)
33
+ request = RequestFactory.new.build_request(metadata, self)
34
+
35
+ response = if mocked
36
+ file_name = File.basename(mock_name || URI.parse(request[:path]).path)
37
+
38
+ MockResponse.new(file_name)
39
+ else
40
+ client.public_send(request[:verb]) do |req|
41
+ req.url request[:path].gsub("//", "/")
42
+ req.headers = request[:headers]
43
+ req.body = request[:payload]
44
+ end
45
+ end
46
+
47
+ body = response.body
48
+ body = "{}" if body.empty?
49
+
50
+ @body = Oj.load(body, symbol_keys: true)
51
+
52
+ if request[:payload]
53
+ metadata[:response][:request] = Oj.load(request[:payload])
54
+ end
55
+
56
+ metadata[:response][:examples] ||= {}
57
+ metadata[:response][:examples]["application/json"] = @body
58
+
59
+ # Validates response matches the proper schema
60
+ Swagalicious::ResponseValidator.new.validate!(metadata, response)
61
+
62
+ response
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,21 @@
1
+ require "json-schema"
2
+
3
+ module Swagalicious
4
+ class ExtendedSchema < JSON::Schema::Draft4
5
+ def initialize
6
+ super
7
+ @attributes["type"] = ExtendedTypeAttribute
8
+ @uri = URI.parse("http://tempuri.org/swagalicious/extended_schema")
9
+ @names = ["http://tempuri.org/swagalicious/extended_schema"]
10
+ end
11
+ end
12
+
13
+ class ExtendedTypeAttribute < JSON::Schema::TypeV4Attribute
14
+ def self.validate(current_schema, data, fragments, processor, validator, options={})
15
+ return if data.nil? && current_schema.schema["null"] == true
16
+ super
17
+ end
18
+ end
19
+
20
+ JSON::Validator.register_validator(ExtendedSchema.new)
21
+ end
@@ -0,0 +1,187 @@
1
+ require "faraday"
2
+ require "faraday/adapter/rack"
3
+
4
+ module Swagalicious
5
+ class RequestFactory
6
+ def initialize(config = ::Swagalicious.config)
7
+ @config = config
8
+ end
9
+
10
+ def build_request(metadata, example)
11
+ swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
12
+ parameters = expand_parameters(metadata, swagger_doc, example)
13
+
14
+ {}.tap do |request|
15
+ add_verb(request, metadata)
16
+ add_path(request, metadata, swagger_doc, parameters, example)
17
+ add_headers(request, metadata, swagger_doc, parameters, example)
18
+ add_payload(request, parameters, example)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def expand_parameters(metadata, swagger_doc, example)
25
+ operation_params = metadata[:operation][:parameters] || []
26
+ path_item_params = metadata[:path_item][:parameters] || []
27
+ security_params = derive_security_params(metadata, swagger_doc)
28
+
29
+ # NOTE: Use of + instead of concat to avoid mutation of the metadata object
30
+ (operation_params + path_item_params + security_params)
31
+ .map { |p| p["$ref"] ? resolve_parameter(p["$ref"], swagger_doc) : p }
32
+ .uniq { |p| p[:name] }
33
+ .reject { |p| p[:required] == false && !example.respond_to?(p[:name]) }
34
+ end
35
+
36
+ def derive_security_params(metadata, swagger_doc)
37
+ requirements = metadata[:operation][:security] || swagger_doc[:security] || []
38
+ scheme_names = requirements.flat_map(&:keys)
39
+ schemes = security_version(scheme_names, swagger_doc)
40
+
41
+ schemes.map do |scheme|
42
+ param = (scheme[:type] == :apiKey) ? scheme.slice(:name, :in) : { name: "Authorization", in: :header }
43
+ param.merge(type: :string, required: requirements.one?)
44
+ end
45
+ end
46
+
47
+ def security_version(scheme_names, swagger_doc)
48
+ if swagger_doc.key?(:securityDefinitions)
49
+ puts "Swagalicious: WARNING: securityDefinitions is replaced in OpenAPI3! Rename to components/securitySchemes (in swagger_helper.rb)"
50
+ swagger_doc[:components] ||= { securitySchemes: swagger_doc[:securityDefinitions] }
51
+ swagger_doc.delete(:securityDefinitions)
52
+ end
53
+ components = swagger_doc[:components] || {}
54
+ (components[:securitySchemes] || {}).slice(*scheme_names).values
55
+ end
56
+
57
+ def resolve_parameter(ref, swagger_doc)
58
+ key = key_version(ref, swagger_doc)
59
+ definitions = definition_version(swagger_doc)
60
+ raise "Referenced parameter '#{ref}' must be defined" unless definitions && definitions[key]
61
+
62
+ definitions[key]
63
+ end
64
+
65
+ def key_version(ref, swagger_doc)
66
+ if ref.start_with?("#/parameters/")
67
+ puts "Swagalicious: WARNING: #/parameters/ refs are replaced in OpenAPI3! Rename to #/components/parameters/"
68
+ ref.sub("#/parameters/", "").to_sym
69
+ else
70
+ ref.sub("#/components/parameters/", "").to_sym
71
+ end
72
+ end
73
+
74
+ def definition_version(swagger_doc)
75
+ if swagger_doc.key?(:parameters)
76
+ puts "Swagalicious: WARNING: parameters is replaced in OpenAPI3! Rename to components/parameters (in swagger_helper.rb)"
77
+ swagger_doc[:parameters]
78
+ else
79
+ components = swagger_doc[:components] || {}
80
+ components[:parameters]
81
+ end
82
+ end
83
+
84
+ def add_verb(request, metadata)
85
+ request[:verb] = metadata[:operation][:verb]
86
+ end
87
+
88
+ def add_path(request, metadata, swagger_doc, parameters, example)
89
+ template = (swagger_doc[:basePath] || "") + metadata[:path_item][:template]
90
+
91
+ request[:path] = template.tap do |path_template|
92
+ parameters.select { |p| p[:in] == :path }.each do |p|
93
+ path_template.gsub!("{#{p[:name]}}", example.send(p[:name]).to_s)
94
+ end
95
+
96
+ parameters.select { |p| p[:in] == :query }.each_with_index do |p, i|
97
+ path_template.concat(i.zero? ? "?" : "&")
98
+ path_template.concat(build_query_string_part(p, example.send(p[:name])))
99
+ end
100
+ end
101
+ end
102
+
103
+ def build_query_string_part(param, value)
104
+ name = param[:name]
105
+ return "#{name}=#{value}" unless param[:type].to_sym == :array
106
+
107
+ case param[:collectionFormat]
108
+ when :ssv
109
+ "#{name}=#{value.join(" ")}"
110
+ when :tsv
111
+ "#{name}=#{value.join("\t")}"
112
+ when :pipes
113
+ "#{name}=#{value.join("|")}"
114
+ when :multi
115
+ value.map { |v| "#{name}=#{v}" }.join("&")
116
+ else
117
+ "#{name}=#{value.join(",")}" # csv is default
118
+ end
119
+ end
120
+
121
+ def add_headers(request, metadata, swagger_doc, parameters, example)
122
+ tuples = parameters
123
+ .select { |p| p[:in] == :header }
124
+ .map { |p| [p[:name], example.send(p[:name]).to_s] }
125
+
126
+ # Accept header
127
+ produces = metadata[:operation][:produces] || swagger_doc[:produces]
128
+ if produces
129
+ accept = example.respond_to?(:Accept) ? example.send(:Accept) : produces.first
130
+ tuples << ["Accept", accept]
131
+ end
132
+
133
+ # Content-Type header
134
+ consumes = metadata[:operation][:consumes] || swagger_doc[:consumes]
135
+ if consumes
136
+ content_type = example.respond_to?(:"Content-Type") ? example.send(:"Content-Type") : consumes.first
137
+ tuples << ["Content-Type", content_type]
138
+ end
139
+
140
+ # Rails test infrastructure requires rackified headers
141
+ rackified_tuples = tuples.map do |pair|
142
+ [
143
+ case pair[0]
144
+ when "Accept" then "HTTP_ACCEPT"
145
+ when "Content-Type" then "CONTENT_TYPE"
146
+ when "Authorization" then "HTTP_AUTHORIZATION"
147
+ else pair[0]
148
+ end,
149
+ pair[1]
150
+ ]
151
+ end
152
+
153
+ request[:headers] = Hash[rackified_tuples]
154
+ end
155
+
156
+ def add_payload(request, parameters, example)
157
+ content_type = request[:headers]["CONTENT_TYPE"]
158
+ return if content_type.nil?
159
+
160
+ if ["application/x-www-form-urlencoded", "multipart/form-data"].include?(content_type)
161
+ request[:payload] = build_form_payload(parameters, example)
162
+ else
163
+ request[:payload] = build_json_payload(parameters, example)
164
+ end
165
+ end
166
+
167
+ def build_form_payload(parameters, example)
168
+ # See http://seejohncode.com/2012/04/29/quick-tip-testing-multipart-uploads-with-rspec/
169
+ # Rather that serializing with the appropriate encoding (e.g. multipart/form-data),
170
+ # Rails test infrastructure allows us to send the values directly as a hash
171
+ # PROS: simple to implement, CONS: serialization/deserialization is bypassed in test
172
+ tuples = parameters
173
+ .select { |p| p[:in] == :formData }
174
+ .map { |p| [p[:name], example.send(p[:name])] }
175
+ Hash[tuples]
176
+ end
177
+
178
+ def build_json_payload(parameters, example)
179
+ body_param = parameters.select { |p| p[:in] == :body }.first
180
+ body_param ? example.send(body_param[:name]).to_json : nil
181
+ end
182
+
183
+ def doc_version(doc)
184
+ doc[:openapi] || doc[:swagger] || "3"
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,64 @@
1
+ require "json"
2
+
3
+ module Swagalicious
4
+ class UnexpectedResponse < StandardError; end
5
+
6
+ class ResponseValidator
7
+ def initialize(config = ::Swagalicious::config)
8
+ @config = config
9
+ end
10
+
11
+ def validate!(metadata, response)
12
+ swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
13
+
14
+ validate_code!(metadata, response.status)
15
+ validate_headers!(metadata, response.headers)
16
+ validate_body!(metadata, swagger_doc, response.body)
17
+ end
18
+
19
+ private
20
+
21
+ def validate_code!(metadata, response)
22
+ expected = metadata[:response][:code].to_s
23
+ if response.to_s != expected.to_s
24
+ raise UnexpectedResponse, "Expected response code '#{response}' to match '#{expected}'"
25
+ end
26
+ end
27
+
28
+ def validate_headers!(metadata, headers)
29
+ expected = (metadata[:response][:headers] || {}).keys
30
+ expected.each do |name|
31
+ raise UnexpectedResponse, "Expected response header #{name} to be present" if headers[name.to_s].nil?
32
+ end
33
+ end
34
+
35
+ def validate_body!(metadata, swagger_doc, body)
36
+ response_schema = metadata[:response][:schema]
37
+ return if response_schema.nil?
38
+
39
+ version = @config.get_swagger_doc_version(metadata[:swagger_doc])
40
+ schemas = definitions_or_component_schemas(swagger_doc, version)
41
+
42
+ validation_schema = response_schema
43
+ .merge("$schema" => "http://tempuri.org/swagalicious/extended_schema")
44
+ .merge(schemas)
45
+
46
+ errors = JSON::Validator.fully_validate(validation_schema, body)
47
+ raise UnexpectedResponse, "Expected response body to match schema: #{errors[0]}" if errors.any?
48
+ end
49
+
50
+ def definitions_or_component_schemas(swagger_doc, version)
51
+ if version.start_with?("2")
52
+ swagger_doc.slice(:definitions)
53
+ else # Openapi3
54
+ if swagger_doc.key?(:definitions)
55
+ puts "Swagger::Specs: WARNING: definitions is replaced in OpenAPI3! Rename to components/schemas (in swagger_helper.rb)"
56
+ swagger_doc.slice(:definitions)
57
+ else
58
+ components = swagger_doc[:components] || {}
59
+ { components: { schemas: components[:schemas] } }
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../core/ext/hash"
4
+
5
+ module Swagalicious
6
+ class SwaggerFormatter
7
+ RSpec::Core::Formatters.register self, :example_group_finished, :stop
8
+
9
+ def initialize(output, config = Swagalicious.config)
10
+ @output = output
11
+ @config = config
12
+
13
+ @output.puts "Generating Swagger docs ..."
14
+ end
15
+
16
+ def example_group_finished(notification)
17
+ metadata = notification.group.metadata
18
+
19
+ # !metadata[:document] won"t work, since nil means we should generate
20
+ # docs.
21
+ return if metadata[:document] == false
22
+ return unless metadata.key?(:response)
23
+
24
+ swagger_doc = @config.get_swagger_doc(metadata[:swagger_doc])
25
+
26
+ # This is called multiple times per file!
27
+ # metadata[:operation] is also re-used between examples within file
28
+ # therefore be careful NOT to modify its content here.
29
+ upgrade_request_type!(metadata)
30
+ upgrade_servers!(swagger_doc)
31
+ upgrade_oauth!(swagger_doc)
32
+ upgrade_response_produces!(swagger_doc, metadata)
33
+
34
+ swagger_doc.deep_merge!(metadata_to_swagger(metadata))
35
+ end
36
+
37
+ def stop(_notification = nil)
38
+ @config.swagger_docs.each do |url_path, doc|
39
+ unless doc_version(doc).start_with?("2")
40
+ doc[:paths]&.each_pair do |_k, v|
41
+ v.each_pair do |_verb, value|
42
+ is_hash = value.is_a?(Hash)
43
+ if is_hash && value.dig(:parameters)
44
+ schema_param = value.dig(:parameters)&.find { |p| (p[:in] == :body || p[:in] == :formData) && p[:schema] }
45
+ mime_list = value.dig(:consumes)
46
+ if value && schema_param && mime_list
47
+ value[:requestBody] = { content: {} } unless value.dig(:requestBody, :content)
48
+ mime_list.each do |mime|
49
+ value[:requestBody][:content][mime] = { schema: schema_param[:schema] }
50
+ end
51
+ end
52
+
53
+ value[:parameters].reject! { |p| p[:in] == :body || p[:in] == :formData }
54
+ end
55
+ remove_invalid_operation_keys!(value)
56
+ end
57
+ end
58
+ end
59
+
60
+ file_path = File.join(@config.swagger_root, url_path)
61
+ dirname = File.dirname(file_path)
62
+ FileUtils.mkdir_p dirname unless File.exist?(dirname)
63
+
64
+ File.open(file_path, "w") do |file|
65
+ file.write(pretty_generate(doc))
66
+ end
67
+
68
+ @output.puts "Swagger doc generated at #{file_path}"
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def pretty_generate(doc)
75
+ if @config.swagger_format == :yaml
76
+ clean_doc = yaml_prepare(doc)
77
+ YAML.dump(clean_doc)
78
+ else # config errors are thrown in "def swagger_format", no throw needed here
79
+ JSON.pretty_generate(doc)
80
+ end
81
+ end
82
+
83
+ def yaml_prepare(doc)
84
+ json_doc = JSON.pretty_generate(doc)
85
+ JSON.parse(json_doc)
86
+ end
87
+
88
+ def metadata_to_swagger(metadata)
89
+ response_code = metadata[:response][:code]
90
+ response = metadata[:response].reject { |k, _v| k == :code }
91
+
92
+ verb = metadata[:operation][:verb]
93
+ operation = metadata[:operation]
94
+ .reject { |k, _v| k == :verb }
95
+ .merge(responses: { response_code => response })
96
+
97
+ path_template = metadata[:path_item][:template]
98
+ path_item = metadata[:path_item]
99
+ .reject { |k, _v| k == :template }
100
+ .merge(verb => operation)
101
+
102
+ { paths: { path_template => path_item } }
103
+ end
104
+
105
+ def doc_version(doc)
106
+ doc[:openapi] || doc[:swagger] || "3"
107
+ end
108
+
109
+ def upgrade_response_produces!(swagger_doc, metadata)
110
+ # Accept header
111
+ mime_list = Array(metadata[:operation][:produces] || swagger_doc[:produces])
112
+ target_node = metadata[:response]
113
+ upgrade_content!(mime_list, target_node)
114
+ metadata[:response].delete(:schema)
115
+ end
116
+
117
+ def upgrade_content!(mime_list, target_node)
118
+ target_node.merge!(content: {})
119
+ schema = target_node[:schema]
120
+ return if mime_list.empty? || schema.nil?
121
+
122
+ mime_list.each do |mime_type|
123
+ # TODO upgrade to have content-type specific schema
124
+ target_node[:content][mime_type] = { schema: schema }
125
+ end
126
+ end
127
+
128
+ def upgrade_request_type!(metadata)
129
+ # No deprecation here as it seems valid to allow type as a shorthand
130
+ operation_nodes = metadata[:operation][:parameters] || []
131
+ path_nodes = metadata[:path_item][:parameters] || []
132
+ header_node = metadata[:response][:headers] || {}
133
+
134
+ (operation_nodes + path_nodes + [header_node]).each do |node|
135
+ if node && node[:type] && node[:schema].nil?
136
+ node[:schema] = { type: node[:type] }
137
+ node.delete(:type)
138
+ end
139
+ end
140
+ end
141
+
142
+ def upgrade_servers!(swagger_doc)
143
+ return unless swagger_doc[:servers].nil? && swagger_doc.key?(:schemes)
144
+
145
+ puts "Swagalicious: WARNING: schemes, host, and basePath are replaced in OpenAPI3! Rename to array of servers[{url}] (in swagger_helper.rb)"
146
+
147
+ swagger_doc[:servers] = { urls: [] }
148
+ swagger_doc[:schemes].each do |scheme|
149
+ swagger_doc[:servers][:urls] << scheme + "://" + swagger_doc[:host] + swagger_doc[:basePath]
150
+ end
151
+
152
+ swagger_doc.delete(:schemes)
153
+ swagger_doc.delete(:host)
154
+ swagger_doc.delete(:basePath)
155
+ end
156
+
157
+ def upgrade_oauth!(swagger_doc)
158
+ # find flow in securitySchemes (securityDefinitions will have been re-written)
159
+ schemes = swagger_doc.dig(:components, :securitySchemes)
160
+ return unless schemes&.any? { |_k, v| v.key?(:flow) }
161
+
162
+ schemes.each do |name, v|
163
+ next unless v.key?(:flow)
164
+
165
+ puts "Swagalicious: WARNING: securityDefinitions flow is replaced in OpenAPI3! Rename to components/securitySchemes/#{name}/flows[] (in swagger_helper.rb)"
166
+
167
+ flow = swagger_doc[:components][:securitySchemes][name].delete(:flow).to_s
168
+
169
+ if flow == "accessCode"
170
+ puts "Swagalicious: WARNING: securityDefinitions accessCode is replaced in OpenAPI3! Rename to clientCredentials (in swagger_helper.rb)"
171
+ flow = "authorizationCode"
172
+ end
173
+
174
+ if flow == "application"
175
+ puts "Swagalicious: WARNING: securityDefinitions application is replaced in OpenAPI3! Rename to authorizationCode (in swagger_helper.rb)"
176
+ flow = "clientCredentials"
177
+ end
178
+
179
+ flow_elements = swagger_doc[:components][:securitySchemes][name].except(:type).each_with_object({}) do |(k, _v), a|
180
+ a[k] = swagger_doc[:components][:securitySchemes][name].delete(k)
181
+ end
182
+
183
+ swagger_doc[:components][:securitySchemes][name].merge!(flows: { flow => flow_elements })
184
+ end
185
+ end
186
+
187
+ def remove_invalid_operation_keys!(value)
188
+ is_hash = value.is_a?(Hash)
189
+ value.delete(:consumes) if is_hash && value.dig(:consumes)
190
+ value.delete(:produces) if is_hash && value.dig(:produces)
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,3 @@
1
+ module Swagalicious
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,32 @@
1
+ require_relative "lib/swagalicious/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "swagalicious"
5
+ spec.version = Swagalicious::VERSION
6
+ spec.authors = ["Eugene Howe"]
7
+ spec.email = ["eugene@xtreme-computers.net"]
8
+
9
+ spec.summary = %q{RSwag without Rails}
10
+ spec.description = %q{This gem is almost a straight copy and paste of https://github.com/rswag/rswag/tree/master/rswag-specs with the Rails specific code stripped out so it can be used in Rack applications that don't use Rails.}
11
+ spec.homepage = "https://github.com/ehowe/swagalicious"
12
+ spec.license = "MIT"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
14
+
15
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
23
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
+ end
25
+ spec.require_paths = ["lib"]
26
+
27
+ spec.add_dependency "faraday", "~> 1.0.1"
28
+ spec.add_dependency "json-schema", "~> 2.8.1"
29
+ spec.add_dependency "oj", "~> 3.10.14"
30
+ spec.add_dependency "rack-test", "~> 1.1.0"
31
+ spec.add_dependency "rspec", "~> 3.9.0"
32
+ end
metadata ADDED
@@ -0,0 +1,140 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: swagalicious
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Eugene Howe
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-09-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.0.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.0.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: json-schema
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 2.8.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 2.8.1
41
+ - !ruby/object:Gem::Dependency
42
+ name: oj
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 3.10.14
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 3.10.14
55
+ - !ruby/object:Gem::Dependency
56
+ name: rack-test
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 1.1.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 1.1.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 3.9.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 3.9.0
83
+ description: This gem is almost a straight copy and paste of https://github.com/rswag/rswag/tree/master/rswag-specs
84
+ with the Rails specific code stripped out so it can be used in Rack applications
85
+ that don't use Rails.
86
+ email:
87
+ - eugene@xtreme-computers.net
88
+ executables: []
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - ".gitignore"
93
+ - ".rspec"
94
+ - ".rubocop.yml"
95
+ - ".travis.yml"
96
+ - Gemfile
97
+ - Gemfile.lock
98
+ - LICENSE.txt
99
+ - README.md
100
+ - Rakefile
101
+ - bin/console
102
+ - bin/setup
103
+ - lib/core/ext/hash.rb
104
+ - lib/swagalicious.rb
105
+ - lib/swagalicious/configuration.rb
106
+ - lib/swagalicious/example_group_helpers.rb
107
+ - lib/swagalicious/example_helpers.rb
108
+ - lib/swagalicious/extended_schema.rb
109
+ - lib/swagalicious/request_factory.rb
110
+ - lib/swagalicious/response_validator.rb
111
+ - lib/swagalicious/swagger_formatter.rb
112
+ - lib/swagalicious/version.rb
113
+ - swagalicious.gemspec
114
+ homepage: https://github.com/ehowe/swagalicious
115
+ licenses:
116
+ - MIT
117
+ metadata:
118
+ allowed_push_host: https://rubygems.org
119
+ homepage_uri: https://github.com/ehowe/swagalicious
120
+ source_code_uri: https://github.com/ehowe/swagalicious
121
+ post_install_message:
122
+ rdoc_options: []
123
+ require_paths:
124
+ - lib
125
+ required_ruby_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: 2.3.0
130
+ required_rubygems_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ requirements: []
136
+ rubygems_version: 3.0.6
137
+ signing_key:
138
+ specification_version: 4
139
+ summary: RSwag without Rails
140
+ test_files: []