Commit dae6fea2 by andrew morton

Get a minimal document built in memory

parent ec0b6ff7
......@@ -16,6 +16,7 @@ rails_gem = case rails_version
gem 'rails', rails_gem
gem 'sqlite3'
gem 'pry'
gem 'pry-byebug'
# Rails 3 requires this but it was removed in Ruby 2.2
gem 'test-unit', '~> 3.0' if rails_major == '3'
......
......@@ -2,6 +2,12 @@
The design of this is heavily influenced by the awesome [swagger_rails](https://github.com/domaindrivendev/swagger_rails) gem.
## Setup
- install gem
- `rails generate rspec:install`
- create `spec/swagger_helper.rb` ... would be nice to be a generator
## Running tests
......
require 'rspec/core'
require 'rspec/swagger/configuration'
require 'rspec/swagger/formatter'
require 'rspec/swagger/helpers'
require 'rspec/swagger/version'
module RSpec
module Swagger
initialize_configuration RSpec.configuration
end
end
module RSpec
module Swagger
# Fake class to document RSpec Swagger configuration options.
class Configuration
end
def self.initialize_configuration(config)
config.add_setting :swagger_root
config.add_setting :swagger_docs, default: {}
Helpers.add_swagger_type_configurations(config)
end
end
end
......@@ -3,33 +3,75 @@ require 'rspec/core/formatters/base_text_formatter'
module RSpec
module Swagger
class Formatter < RSpec::Core::Formatters::BaseTextFormatter
RSpec::Core::Formatters.register self,
:example_group_started, :example_group_finished, :example_finished
RSpec::Core::Formatters.register self, :example_finished, :close
def initialize(output)
super
@document = {}
end
def watching?
!!@watching
def documents
# We don't try to load the docs in `initalize` because when running
# `rspec -f RSpec::Swagger::Formatter` RSpec initalized this class
# before `swagger_helper` has run.
@documents ||= ::RSpec.configuration.swagger_docs
end
def example_group_started(notification)
@watching = notification.group.metadata[:type] == :request
return unless watching?
def example_finished(notification)
return unless notification.example.metadata[:swagger_object] == :response
data = notification.example.metadata[:swagger_data]
document = document_for(nil)
path = path_for(document, data[:path])
operation = operation_for(path, data[:operation])
response = response_for(operation, data[:status_code])
response[:description] = data[:response_description] if data[:response_description]
response[:examples] = prepare_example(data[:example]) if data[:example]
pp notification.group.metadata
# notification.example.metadata.each do |k, v|
# puts "#{k}\t#{v}" if k.to_s.starts_with?("swagger")
# end
end
def example_finished(notification)
return unless watching?
def close(_notification)
documents.each{|k, v| write_json(k, v)}
end
# pp notification
def write_json(name, document)
pp document
end
def example_group_finished(notification)
@watching = true
def document_for doc_name = nil
if doc_name
documents.fetch(doc_name)
else
documents.values.first
end
end
def path_for document, path_name
document[:paths] ||= {}
document[:paths][path_name] ||= {}
end
def operation_for path, operation_name
path[operation_name] ||= {responses: {}}
end
def response_for operation, status_code
operation[:responses][status_code] ||= {}
end
def prepare_example example
mime_type = example[:content_type]
body = example[:body]
if mime_type == 'application/json'
begin
body = JSON.parse(body)
rescue JSON::ParserError => e
end
end
{ mime_type => body }
end
end
end
......
module RSpec
module Swagger
module Helpers
# The helpers serve as a DSL.
def self.add_swagger_type_configurations(config)
# The filters are used to ensure that the methods are nested correctly
# and following the Swagger schema.
config.extend Paths, type: :request
config.extend PathItem, swagger_object: :path_item
config.extend Parameters, swagger_object: :path_item
config.extend Operation, swagger_object: :operation
config.extend Parameters, swagger_object: :operation
config.extend Response, swagger_object: :status_code
end
=begin
paths: (Paths)
/pets: (Path Item)
post: (Operation)
tags:
- pet
summary: Add a new pet to the store
description: ""
operationId: addPet
consumes:
- application/json
- application/xml
produces:
- application/json
- application/xml
parameters: (Parameters)
- in: body
name: body
description: Pet object that needs to be added to the store
required: false
schema:
$ref: "#/definitions/Pet"
responses: (Responses)
"405": (Response)
description: Invalid input
=end
module Paths
def path template, &block
#TODO template might be a $ref
meta = {
swagger_object: :path_item,
swagger_data: {path: template}
}
describe("path #{template}", meta, &block)
end
end
module PathItem
def operation verb, desc, &block
verb = verb.to_s.downcase
meta = {
swagger_object: :operation,
swagger_data: metadata[:swagger_data].merge(operation: verb.to_sym, operation_description: desc)
}
describe(verb.to_s, meta, &block)
end
end
module Parameters
def parameter name, args = {}
# TODO these should be unique by (name, in) so we should use a hash
# to store them instead of an array.
param_key = "#{metadata[:swagger_object]}_params".to_s
params = metadata[:swagger_data][param_key] ||= []
params << { name: name }.merge(args)
end
end
module Operation
def response code, desc, params = {}, headers = {}, &block
meta = {
swagger_object: :status_code,
swagger_data: metadata[:swagger_data].merge(status_code: code, response_description: desc)
}
describe("#{code}", meta) do
self.module_exec(&block) if block_given?
method = metadata[:swagger_data][:operation]
path = metadata[:swagger_data][:path]
args = if ::Rails::VERSION::MAJOR >= 5
[path, {params: params, headers: headers}]
else
[path, params, headers]
end
# TODO: this needs a better mechanism
if metadata[:capture_example]
example = metadata[:swagger_data][:example] = {}
end
meta = {
swagger_object: :response
# response: metadata[:swagger_data][:response].merge()
}
it("matches", meta) do
self.send(method, *args)
if response && example
example.merge!( body: response.body, content_type: response.content_type.to_s)
end
expect(response).to have_http_status(code)
end
end
end
end
module Response
def capture_example
metadata[:capture_example] = true
end
end
end
end
end
#!/bin/bash
set -x
# export RAILS_VERSION=3.2.0
......
require 'rails_helper'
require 'swagger_helper'
module SwaggerPath
def path template, &block
describe "path #{template}", {swagger_path: template}, &block
end
end
module SwaggerOperation
def operation verb, &block
verb = verb.to_s.downcase
describe verb.to_s, {swagger_operation: verb.to_sym}, &block
end
end
module SwaggerRequest
def test_request code, params = {}, headers = {}
path = metadata[:swagger_path]
method = metadata[:swagger_operation]
# binding.pry
args = if Rails::VERSION::MAJOR >= 5
[path, { params: params, headers: headers }]
else
[path, params, headers]
RSpec.describe "Requestsing", type: :request do
path '/posts' do
operation "GET", "fetch list" do
# params
response(200, "successful", {}, {'CONTENT_TYPE' => 'application/json', 'HTTP_ACCEPT' => 'application/json'})
end
it "does stuff" do
# binding.pry
self.send(method, *args)
expect(response).to have_http_status(code)
operation "POST", "create" do
# params
response(201, "successfully created", { post: { title: 'asdf', body: "blah" } }.to_json, {'CONTENT_TYPE' => 'application/json', 'HTTP_ACCEPT' => 'application/json'}) do
capture_example
end
end
end
end
RSpec.configure do |c|
c.extend SwaggerPath, type: :request
c.extend SwaggerOperation, :swagger_path
c.extend SwaggerRequest, :swagger_operation
end
RSpec.describe "Requestsing", type: :request do
path '/posts' do
operation "GET" do
test_request(200, {}, {'CONTENT_TYPE' => 'application/json', 'HTTP_ACCEPT' => 'application/json'})
end
operation "POST" do
test_request(201, { post: { title: 'asdf', body: "blah" } }.to_json, {'CONTENT_TYPE' => 'application/json', 'HTTP_ACCEPT' => 'application/json'})
path '/posts/1' do
parameter "path-param", {in: :path}
operation "GET", "fetch item" do
before { Post.new.save }
parameter "op-param"
response(200, "success", {}, {'CONTENT_TYPE' => 'application/json', 'HTTP_ACCEPT' => 'application/json'}) do
capture_example
end
end
end
end
......@@ -4,41 +4,93 @@ RSpec.describe RSpec::Swagger::Formatter do
let(:output) { StringIO.new }
let(:formatter) { described_class.new(output) }
let(:group_notification) { double(group: group) }
let(:group) { double(metadata: metadata) }
let(:metadata) { {} }
let(:documents) do
{
'minimal.json' => {
swagger: '2.0',
info: {
version: '0.0.0',
title: 'Simple API'
}
}
}
end
describe "#example_group_started" do
context "groups with no type" do
let(:metadata) { {} }
before do
RSpec.configure {|c| c.swagger_docs = documents }
end
it "ignores" do
formatter.example_group_started(group_notification)
describe "#example_finished" do
let(:example_notification) { double('Notification', example: double('Example', metadata: metadata)) }
let(:metadata) { {} }
expect(formatter).not_to be_watching
context "minimal" do
let(:metadata) do
{
swagger_object: :response,
swagger_data: {path: "/ping", operation: :put, status_code: 200, response_description: 'OK', example: nil}
}
end
end
context "groups with type request" do
let(:metadata) { {type: :request} }
it "copies the requests into the document" do
formatter.example_finished(example_notification)
it "watches" do
formatter.example_group_started(group_notification)
expect(formatter).to be_watching
expect(formatter.documents.values.first).to eq({
swagger: '2.0',
info: {
version: '0.0.0',
title: 'Simple API'
},
paths: {
'/ping' => {
put: {
responses: {200 => {description: 'OK'}}
}
}
}
})
end
end
end
describe "#example_group_finished" do
let(:metadata) { {} }
describe "#close" do
let(:blank_notification) { double('Notification') }
context "no relevant examples" do
it "writes document with no changes" do
expect(formatter).to receive(:write_json).with(documents.keys.first, documents.values.first)
formatter.close(blank_notification)
end
end
context "with a relevant example" do
let(:example_notification) { double(example: double(metadata: metadata)) }
let(:metadata) do
{
swagger_object: :response,
swagger_data: {path: '/ping', operation: :get, status_code: 200, response_description: 'all good' }
}
end
it "resets to watching" do
formatter.example_group_started(group_notification)
expect(formatter).not_to be_watching
it "writes a document with the request" do
formatter.example_finished(example_notification)
formatter.example_group_finished(group_notification)
expect(formatter).to be_watching
expect(formatter).to receive(:write_json).with(
documents.keys.first,
documents.values.first.merge({
paths: {
'/ping' => {
get: {
responses: {200 => {description: 'all good'}}
}
}
}
})
)
formatter.close(blank_notification)
end
end
end
end
require 'spec_helper'
require 'swagger_helper'
RSpec.describe RSpec::Swagger do
it "loads" do
......
require 'rspec/swagger'
# This file was generated by the `rspec --init` command. Conventionally, all
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
# The generated `.rspec` file contains `--require spec_helper` which will cause
......
require 'rspec/swagger'
require 'rails_helper'
RSpec.configure do |config|
# Specify a root folder where Swagger JSON files are generated
config.swagger_root = Rails.root.to_s + '/swagger'
# Define one or more Swagger documents and global metadata for each.
#
# When you run the "swaggerize" rake task, the complete Swagger will be
# generated at the provided relative path under `swagger_root`
# By default, the operations defined in spec files are added to the first
# document below. You can override this behavior by adding a swagger_doc tag to the
# the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json'
config.swagger_docs = {
'v1/swagger.json' => {
swagger: '2.0',
info: {
title: 'API V1',
version: 'v1'
}
}
}
end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment