Created
March 23, 2016 08:55
-
-
Save nikone/de4fb72b50287354528a to your computer and use it in GitHub Desktop.
Api Blueprint test docs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# lib/api_blueprint.rb | |
require 'rspec/core/formatters/base_formatter' | |
require 'pry' | |
class ApiBlueprintPrinter | |
def initialize(output) | |
@output = output | |
end | |
def print_meta_info | |
@output.puts("# Tiger Trade API Documentation") | |
end | |
def print_resource_group(resource_group_name, resource_group_resources) | |
@output.puts "# Group #{resource_group_name}" | |
resource_group_resources.each(&method(:print_resource)) | |
end | |
def print_resource(resource_name, actions) | |
unless resource_name =~ /^[^\[\]]*\[\/[^\]]+\]/ | |
raise "resource: '#{resource_name}' is invalid. :resource needs to be specified according to https://github.com/apiaryio/api-blueprint/blob/master/API%20Blueprint%20Specification.md#resource-section" | |
end | |
@output.puts "# #{resource_name}" | |
http_verbs = actions.keys.map {|action| action.scan(/\[([A-Z]+.*)\]/).flatten[0] } | |
unless http_verbs.length == http_verbs.uniq.length | |
raise "Action HTTP verbs are not unique #{actions.keys.inspect} for resource: '#{resource_name}'" | |
end | |
actions.each(&method(:print_action)) | |
end | |
def print_action(action_name, action_meta_data) | |
@output.puts "## #{action_name}\n" \ | |
"\n" \ | |
"#{action_meta_data[:description]}\n" \ | |
"\n" \ | |
uri_params = action_meta_data[:action_parameters].map do |param| | |
" + #{param[:key]}: #{param[:value]} (#{param[:type]}, #{param[:required]}) - #{param[:description]}" | |
end | |
if uri_params.present? | |
@output.puts(["+ Parameters", uri_params].flatten.join("\n")) | |
end | |
action_meta_data[:examples].each(&method(:print_example)) | |
end | |
def print_example(_example_description, example_metadata) | |
@output.puts "+ Request #{example_metadata[:request][:identifier]} (#{example_metadata[:request][:format]})" | |
if example_metadata[:request][:parameters].present? | |
@output.puts "\n" \ | |
"#{indent_lines(8, pretty_json(example_metadata[:request][:parameters]))}\n" \ | |
end | |
@output.puts "+ Response #{example_metadata[:response][:status]} (#{example_metadata[:request][:format]})" | |
if example_metadata[:response][:body].present? | |
@output.puts "\n" \ | |
"#{indent_lines(8, pretty_json(safe_json_parse(example_metadata[:response][:body])))}\n" \ | |
"\n" | |
end | |
end | |
def pretty_json(json_string) | |
if json_string.present? | |
JSON.pretty_generate(json_string) | |
else | |
'' | |
end | |
end | |
def safe_json_parse(json_string) | |
json_string.length >= 2 ? JSON.parse(json_string) : nil | |
end | |
def indent_lines(number_of_spaces, string) | |
string | |
.split("\n") | |
.map { |a| a.prepend(' ' * number_of_spaces) } | |
.join("\n") | |
end | |
end | |
class ApiBlueprint < RSpec::Core::Formatters::BaseFormatter | |
RSpec::Core::Formatters.register self, :example_passed, :example_started, :stop | |
def initialize(output) | |
super | |
@passed_examples = {} | |
@group_level = 0 | |
end | |
def example_started(notification) | |
@example_group_instance = notification.example.example_group_instance | |
end | |
def example_passed(passed) | |
metadata = passed.example.metadata | |
if should_document_example?(metadata) | |
move_example_to_passed(metadata) | |
end | |
@example_group_instance = nil | |
end | |
def stop(_notification) | |
printer.print_meta_info | |
@passed_examples.sort_by { |k, _v| k }.each do |resource_group_name, resource_group_resources| | |
printer.print_resource_group(resource_group_name, resource_group_resources) | |
end | |
end | |
private | |
def move_example_to_passed(metadata) | |
@passed_examples.deep_merge!({ | |
metadata[:resource_group] => { | |
metadata[:resource] => { | |
metadata[:action] => { | |
description: metadata[:action_description], | |
action_parameters: metadata[:action_parameters] || [], | |
examples: { | |
metadata[:description] => { | |
request: { | |
parameters: request.parameters.except(*request.path_parameters.keys.map(&:to_s)).except(*request.query_parameters.keys.map(&:to_s)), | |
format: request.format, | |
identifier: fullpath | |
}, | |
response: { | |
status: response.status, | |
body: response.body | |
} | |
} | |
} | |
} | |
} | |
} | |
}) | |
end | |
def should_document_example?(metadata) | |
metadata[:apidoc] && | |
metadata[:resource_group] && | |
metadata[:resource] && | |
metadata[:action] && | |
metadata[:action_description] && | |
!metadata[:nodoc] | |
end | |
def request | |
@example_group_instance.request | |
end | |
def response | |
@example_group_instance.response | |
end | |
def path | |
request.path.sub(/\.json[^\?]*/, '') | |
end | |
def fullpath | |
if request.query_parameters.present? | |
URI::HTTP.build(path: path, query: request.query_parameters.to_query) | |
else | |
path | |
end | |
end | |
def printer | |
@printer ||= ApiBlueprintPrinter.new(output) | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# lib/tasks/api_docs.rake | |
task generate_api_docs: :environment do | |
`bundle exec rspec spec --tag apidoc -f ApiBlueprint --order defined --out spec/apispec.md` | |
`aglio -i spec/apispec.md -o public/api/docs/index.html` | |
`aglio --theme-variables slate -i spec/apispec.md -o public/api/docs/index-dark.html` | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# lib/blueprint_api_doc.rb | |
require 'active_support/concern' | |
require 'active_support/inflector' | |
module BlueprintApiDoc | |
module DSL | |
module AttrProxy | |
def method_missing(name, value) | |
setter = "#{name.to_s}=" | |
return super unless respond_to? setter | |
public_send setter, value | |
end | |
end | |
class Documentation | |
attr_accessor :subject | |
attr_accessor :_resource | |
attr_accessor :_action | |
def initialize(opts = {}) | |
self.subject = opts.fetch :subject | |
end | |
def resource(name = nil, &block) | |
self._resource = Resource.new(name: name) | |
_resource.instance_eval(&block) | |
end | |
def action(name = nil, &block) | |
self._action = Action.new(name: name) | |
_action.instance_eval(&block) | |
end | |
def config | |
{}.merge(_resource ? _resource.config : {}) | |
.merge(_action ? _action.config : {}) | |
end | |
end | |
class Resource | |
include AttrProxy | |
attr_writer :name | |
attr_writer :group | |
def initialize(opts = {}) | |
self.name = opts.fetch(:name, nil) | |
self.group = opts.fetch(:group, nil) | |
end | |
def config | |
{}.tap do |config| | |
config[:resource] = @name if @name | |
config[:resource_group] = @group if @group | |
config[:apidoc] = true | |
end | |
end | |
end | |
class Action | |
include AttrProxy | |
attr_writer :name | |
attr_writer :desc | |
attr_writer :params | |
def initialize(opts = {}) | |
self.name = opts.fetch(:name, nil) | |
self.desc = opts.fetch(:desc, nil) | |
self.params = opts.fetch(:params, nil) | |
end | |
def param(signature) | |
params << signature | |
end | |
def config | |
Hash.new.tap do |config| | |
config[:action] = @name if @name | |
config[:action_description] = @desc if @desc | |
config[:action_parameters] = @params if @params | |
end | |
end | |
end | |
module Syntax | |
extend ActiveSupport::Concern | |
class_methods do | |
def document(subject, &block) | |
documentation = _subjects[subject] = Documentation.new(subject: subject) | |
documentation.instance_eval(&block) | |
end | |
def const_missing(name) | |
documentation = _subjects[infer_subject(name)] | |
return super unless documentation | |
Module.new do | |
define_singleton_method :included do |base| | |
base.metadata.merge! documentation.config | |
end | |
end | |
end | |
def infer_subject(name) | |
name.to_s.underscore.to_sym | |
end | |
def _subjects | |
@_subjects ||= {} | |
end | |
end | |
end | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# spec/support/api_doc/v1/categories | |
module ApiDoc | |
module V1 | |
module Categories | |
include BlueprintApiDoc::DSL::Syntax | |
category_params = [{ key: :id, type: :number, required: :required, value: 1, description: 'category id' }] | |
document :api do | |
resource do | |
name 'Categories [/categories]' | |
group 'Categories' | |
end | |
end | |
document :index do | |
action do | |
name 'Get categories [GET /categories]' | |
desc 'Returns list of categories' | |
end | |
end | |
document :show do | |
action do | |
name 'Get category [GET /categories/{id}]' | |
desc 'Returns category' | |
params category_params | |
end | |
end | |
document :create do | |
action do | |
name 'Post category [POST /categories]' | |
desc 'Creates category' | |
end | |
end | |
document :update do | |
action do | |
name 'Patch category [PATCH /categories/{id}]' | |
desc 'Updates category' | |
params category_params | |
end | |
end | |
document :destroy do | |
action do | |
name 'Delete category [DELETE /categories/{id}]' | |
desc 'Deletes category' | |
params category_params | |
end | |
end | |
end | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# spec/controllers/api/v1/categories_controller_spec.rb | |
describe Api::V1::CategoriesController, type: :controller do | |
include ApiDoc::V1::Categories::Api | |
let(:category) { create(:category) } | |
describe 'GET #index' do | |
include ApiDoc::V1::Categories::Index | |
before { category } | |
it 'returns a list of categories' do | |
get :index | |
expect(response).to have_http_status(:ok) | |
expect(response_body).to be_jsonapi_collection_for('categories') | |
end | |
end | |
describe 'GET #show' do | |
include ApiDoc::V1::Categories::Show | |
it 'returns category' do | |
get :show, id: category.id | |
expect(response).to have_http_status(:ok) | |
expect(response_body).to be_jsonapi_resource_for('categories') | |
end | |
context 'invalid id' do | |
it 'returns not found error' do | |
get :show, id: 9808789 | |
expect(response).to have_http_status(:not_found) | |
expect(response).to be_jsonapi_error | |
end | |
end | |
end | |
describe 'POST #create' do | |
include ApiDoc::V1::Categories::Create | |
context 'valid parameters' do | |
let(:params) { { name: 'Jewlery' } } | |
it 'creates category' do | |
expect { | |
post :create, data: { attributes: params } | |
}.to change(Category, :count).by(1) | |
expect(response).to have_http_status(:created) | |
expect(response_body).to be_jsonapi_resource_for('categories') | |
end | |
end | |
context 'invalid parameters' do | |
it 'returns bad request error' do | |
expect { | |
post :create, data: { attributes: {} } | |
}.to change(Category, :count).by(0) | |
expect(response).to have_http_status(:unprocessable_entity) | |
expect(response).to be_jsonapi_error | |
end | |
end | |
end | |
describe 'PATCH #update' do | |
include ApiDoc::V1::Categories::Update | |
context 'valid parameters' do | |
let(:params) { { name: 'Jewlery' } } | |
it 'updates category' do | |
patch :update, id: category.id, data: { attributes: params } | |
expect(response).to have_http_status(:ok) | |
expect(response_body).to be_jsonapi_resource_for('categories') | |
end | |
end | |
context 'invalid parameters' do | |
let(:params) { { name: '' } } | |
it 'returns bad request error' do | |
patch :update, id: category.id, data: { attributes: params } | |
expect(response).to have_http_status(:unprocessable_entity) | |
expect(response).to be_jsonapi_error | |
end | |
end | |
end | |
describe 'DELETE #destroy' do | |
include ApiDoc::V1::Categories::Destroy | |
it 'deletes category' do | |
delete :destroy, id: category.id | |
expect(response).to have_http_status(:no_content) | |
expect(response.body).to eq('') | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment