apiculture 0.0.19 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +8 -5
- data/LICENSE.txt +1 -1
- data/README.md +10 -11
- data/apiculture.gemspec +6 -6
- data/gemfiles/Gemfile.rack-1.x +17 -0
- data/gemfiles/Gemfile.rack-2.x +17 -0
- data/lib/apiculture/action.rb +2 -2
- data/lib/apiculture/app.rb +131 -0
- data/lib/apiculture/indifferent_hash.rb +155 -0
- data/lib/apiculture/sinatra_instance_methods.rb +2 -3
- data/lib/apiculture/version.rb +1 -1
- data/lib/apiculture.rb +2 -0
- data/spec/apiculture/action_spec.rb +17 -17
- data/spec/apiculture/app_documentation_spec.rb +4 -4
- data/spec/apiculture_spec.rb +101 -115
- data/spec/spec_helper.rb +0 -1
- metadata +12 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aa9f65f24dcec58c1fa839c4b06ad6707ff94929
|
4
|
+
data.tar.gz: 972227095a632256fba17ff766701d6626135f19
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e4f2f860e3ede4b6969b6a42cd25157ff58ac1f9d5c9fa37d984e4e2eabe48b35c912efd358584a0392655c292e392feaed2e69fd13d992dba418419541c8139
|
7
|
+
data.tar.gz: 03754e831d7b7a2e44fdb431906ffb6119d78ee1263a2978caf89e43983f216ad5f1d8ad61e331fcdedfe03f491ce9ff996cb6c39034fc78234adae370bccf91
|
data/.travis.yml
CHANGED
@@ -1,10 +1,13 @@
|
|
1
|
+
gemfile:
|
2
|
+
- gemfiles/Gemfile.rack-1.x
|
3
|
+
- gemfiles/Gemfile.rack-2.x
|
1
4
|
rvm:
|
2
|
-
- 2.
|
3
|
-
- 2.
|
4
|
-
- 2.
|
5
|
-
- 2.
|
5
|
+
- 2.3.7
|
6
|
+
- 2.4.4
|
7
|
+
- 2.5.1
|
8
|
+
- 2.6.0-preview1
|
6
9
|
sudo: false
|
7
10
|
cache: bundler
|
8
11
|
matrix:
|
9
12
|
allow_failures:
|
10
|
-
- rvm: 2.
|
13
|
+
- rvm: 2.6.0-preview1
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -1,31 +1,31 @@
|
|
1
1
|
# apiculture
|
2
2
|
|
3
|
-
A little toolkit for building RESTful API backends on top of
|
3
|
+
A little toolkit for building RESTful API backends on top of Rack.
|
4
4
|
|
5
5
|
[![Build Status](https://travis-ci.org/WeTransfer/apiculture.svg?branch=master)](https://travis-ci.org/WeTransfer/apiculture)
|
6
6
|
|
7
7
|
## Ideas
|
8
8
|
|
9
9
|
A simple API definition DSL with simple premises:
|
10
|
-
|
10
|
+
|
11
11
|
* Endpoint URLs should be _visible_ in the actual code. The reason for that is with nested
|
12
12
|
blocks you inevitably end up setting up context somewhere far away from the terminal route
|
13
13
|
that ends up using that context.
|
14
14
|
* Explicit allowed/required parameters (both payload/query string and body)
|
15
15
|
* Explicit description in front of the API action definition
|
16
16
|
* Wrap the actual work into Actions, so that the API definition is mostly routes
|
17
|
-
|
17
|
+
|
18
18
|
## A taste of honey
|
19
19
|
|
20
20
|
```ruby
|
21
|
-
class Api::V2 <
|
22
|
-
|
21
|
+
class Api::V2 < Apiculture::App
|
22
|
+
|
23
23
|
use Rack::Parser, :content_types => {
|
24
24
|
'application/json' => JSON.method(:load).to_proc
|
25
25
|
}
|
26
|
-
|
26
|
+
|
27
27
|
extend Apiculture
|
28
|
-
|
28
|
+
|
29
29
|
desc 'Create a Contact'
|
30
30
|
required_param :name, 'Name of the person', String
|
31
31
|
param :email, 'Email address of the person', String
|
@@ -36,7 +36,7 @@ class Api::V2 < Sinatra::Base
|
|
36
36
|
# works exactly the same - but we suggest using Actions instead.
|
37
37
|
action_result CreateContact # uses Api::V2::CreateContact
|
38
38
|
end
|
39
|
-
|
39
|
+
|
40
40
|
desc 'Fetch a Contact'
|
41
41
|
route_param :id, 'ID of the person'
|
42
42
|
responds_with 200, 'Contact data', {name: 'John Appleseed', id: "ac19...fefg"}
|
@@ -75,7 +75,7 @@ If you want to also examine the HTML documentation that gets built during the te
|
|
75
75
|
Note that this requires presence of the `open` commandline utility (should be available on both OSX and Linux).
|
76
76
|
|
77
77
|
## Contributing to apiculture
|
78
|
-
|
78
|
+
|
79
79
|
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
80
80
|
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
81
81
|
* Fork the project.
|
@@ -86,6 +86,5 @@ Note that this requires presence of the `open` commandline utility (should be av
|
|
86
86
|
|
87
87
|
## Copyright
|
88
88
|
|
89
|
-
Copyright (c) 2015 WeTransfer. See LICENSE.txt for
|
89
|
+
Copyright (c) 2015-2018 WeTransfer. See LICENSE.txt for
|
90
90
|
further details.
|
91
|
-
|
data/apiculture.gemspec
CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |s|
|
|
10
10
|
s.require_paths = ["lib"]
|
11
11
|
s.authors = ["Julik Tarkhanov", "WeTransfer"]
|
12
12
|
s.homepage = 'http://github.com/wetransfer/zip_tricks'
|
13
|
-
s.description = "A toolkit for building REST APIs on top of
|
13
|
+
s.description = "A toolkit for building REST APIs on top of Rack"
|
14
14
|
s.email = "me@julik.nl"
|
15
15
|
|
16
16
|
# Prevent pushing this gem to RubyGems.org.
|
@@ -22,7 +22,7 @@ Gem::Specification.new do |s|
|
|
22
22
|
raise 'RubyGems 2.0 or newer is required to protect against ' \
|
23
23
|
'public gem pushes.'
|
24
24
|
end
|
25
|
-
|
25
|
+
|
26
26
|
s.files = `git ls-files -z`.split("\x0")
|
27
27
|
s.extra_rdoc_files = [
|
28
28
|
"LICENSE.txt",
|
@@ -31,17 +31,17 @@ Gem::Specification.new do |s|
|
|
31
31
|
s.homepage = "https://github.com/WeTransfer/apiculture"
|
32
32
|
s.licenses = ["MIT"]
|
33
33
|
s.rubygems_version = "2.4.5.1"
|
34
|
-
s.summary = "Sweet API sauce on top of
|
34
|
+
s.summary = "Sweet API sauce on top of Rack"
|
35
35
|
|
36
|
-
s.add_runtime_dependency '
|
36
|
+
s.add_runtime_dependency 'mustermann', '~> 1'
|
37
37
|
s.add_runtime_dependency 'builder', '~> 3'
|
38
38
|
s.add_runtime_dependency 'rdiscount', '~> 2'
|
39
39
|
s.add_runtime_dependency 'github-markup', '~> 1'
|
40
|
-
s.add_runtime_dependency
|
40
|
+
s.add_runtime_dependency 'mustache', '~> 1'
|
41
41
|
|
42
42
|
s.add_development_dependency 'rack-test'
|
43
43
|
s.add_development_dependency "rspec", "~> 3.1", '< 3.2'
|
44
|
-
s.add_development_dependency "rdoc", "~>
|
44
|
+
s.add_development_dependency "rdoc", "~> 6.0"
|
45
45
|
s.add_development_dependency "rake", "~> 10"
|
46
46
|
s.add_development_dependency "bundler", "~> 1.0"
|
47
47
|
s.add_development_dependency "simplecov", ">= 0"
|
@@ -0,0 +1,17 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
gem 'rack', "~> 1"
|
3
|
+
|
4
|
+
gem 'mustermann', '~> 1'
|
5
|
+
gem 'builder', '~> 3'
|
6
|
+
gem 'rdiscount', '~> 2'
|
7
|
+
gem 'github-markup', '~> 1'
|
8
|
+
gem 'mustache', '~> 1'
|
9
|
+
|
10
|
+
group :development do
|
11
|
+
gem 'rack-test'
|
12
|
+
gem "rspec", "~> 3.1", '< 3.2'
|
13
|
+
gem "rdoc", "~> 6.0"
|
14
|
+
gem "rake", "~> 10"
|
15
|
+
gem "bundler", "~> 1.0"
|
16
|
+
gem "simplecov", ">= 0"
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
gem 'rack', "~> 2"
|
3
|
+
|
4
|
+
gem 'mustermann', '~> 1'
|
5
|
+
gem 'builder', '~> 3'
|
6
|
+
gem 'rdiscount', '~> 2'
|
7
|
+
gem 'github-markup', '~> 1'
|
8
|
+
gem 'mustache', '~> 1'
|
9
|
+
|
10
|
+
group :development do
|
11
|
+
gem 'rack-test'
|
12
|
+
gem "rspec", "~> 3.1", '< 3.2'
|
13
|
+
gem "rdoc", "~> 6.0"
|
14
|
+
gem "rake", "~> 10"
|
15
|
+
gem "bundler", "~> 1.0"
|
16
|
+
gem "simplecov", ">= 0"
|
17
|
+
end
|
data/lib/apiculture/action.rb
CHANGED
@@ -11,9 +11,9 @@
|
|
11
11
|
class Apiculture::Action
|
12
12
|
# Initialize a new BasicAction, with the given Sintra application and a hash
|
13
13
|
# of keyword arguments that will be converted into instance variables.
|
14
|
-
def initialize(
|
14
|
+
def initialize(app_receiver, **ivars)
|
15
15
|
ivars.each_pair {|k,v| instance_variable_set("@#{k}", v) }
|
16
|
-
@_sinatra_app =
|
16
|
+
@_sinatra_app = app_receiver
|
17
17
|
end
|
18
18
|
|
19
19
|
# Halt with a JSON error message (delegates to Sinatra's halt() under the hood)
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require'mustermann'
|
2
|
+
|
3
|
+
class Apiculture::App
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def use(middlreware_factory, middleware_options, &middleware_blk)
|
7
|
+
@middleware_configurations ||= []
|
8
|
+
@middleware_configurations << [middleware_factory, middleware_options, middleware_blk]
|
9
|
+
end
|
10
|
+
|
11
|
+
def middleware_configurations
|
12
|
+
@middleware_configurations || []
|
13
|
+
end
|
14
|
+
|
15
|
+
def get(url, **options, &handler_blk)
|
16
|
+
define_action :get, url, options, &handler_blk
|
17
|
+
end
|
18
|
+
|
19
|
+
def post(url, **options, &handler_blk)
|
20
|
+
define_action :post, url, options, &handler_blk
|
21
|
+
end
|
22
|
+
|
23
|
+
def put(url, **options, &handler_blk)
|
24
|
+
define_action :put, url, options, &handler_blk
|
25
|
+
end
|
26
|
+
|
27
|
+
def delete(url, **options, &handler_blk)
|
28
|
+
define_action :delete, url, options, &handler_blk
|
29
|
+
end
|
30
|
+
|
31
|
+
def actions
|
32
|
+
@actions || []
|
33
|
+
end
|
34
|
+
|
35
|
+
def define_action(http_method, url_path, **options, &handler_blk)
|
36
|
+
@actions ||= []
|
37
|
+
@actions << [http_method.to_s.upcase, url_path, options, handler_blk]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def call_without_middleware(env)
|
42
|
+
@env = env
|
43
|
+
|
44
|
+
# First try to route via actions...
|
45
|
+
given_http_method = env.fetch('REQUEST_METHOD')
|
46
|
+
given_path = env.fetch('PATH_INFO')
|
47
|
+
given_path = '/' + given_path unless given_path.start_with?('/')
|
48
|
+
|
49
|
+
action_list = self.class.actions
|
50
|
+
# TODO: I believe Sinatra matches bottom-up, not top-down.
|
51
|
+
action_list.reverse.each do | (action_http_method, action_url_path, action_options, action_handler_callable)|
|
52
|
+
route_pattern = Mustermann.new(action_url_path)
|
53
|
+
if given_http_method == action_http_method && route_params = route_pattern.params(given_path)
|
54
|
+
@request = Rack::Request.new(env)
|
55
|
+
@params.merge!(@request.params)
|
56
|
+
@route_params = route_params
|
57
|
+
|
58
|
+
match = route_pattern.match(given_path)
|
59
|
+
@route_params['captures'] = match.captures unless match.nil?
|
60
|
+
@params.merge!(@route_params)
|
61
|
+
return perform_action_block(&action_handler_callable)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# and if nothing works out - respond with a 404
|
66
|
+
out = JSON.pretty_generate({
|
67
|
+
error: 'No matching action found for %s %s' % [given_http_method, given_path],
|
68
|
+
})
|
69
|
+
[404, {'Content-Type' => 'application/json', 'Content-Length' => out.bytesize.to_s}, [out]]
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.call(env)
|
73
|
+
app = new
|
74
|
+
Rack::Builder.new do
|
75
|
+
(@middleware_configurations || []).each do |middleware_args|
|
76
|
+
use(*middleware_args)
|
77
|
+
end
|
78
|
+
run ->(env) { app.call_without_middleware(env) }
|
79
|
+
end.to_app.call(env)
|
80
|
+
end
|
81
|
+
|
82
|
+
attr_reader :request
|
83
|
+
attr_reader :env
|
84
|
+
attr_reader :params
|
85
|
+
|
86
|
+
def initialize
|
87
|
+
@status = 200
|
88
|
+
@content_type = 'text/plain'
|
89
|
+
@params = Apiculture::IndifferentHash.new
|
90
|
+
end
|
91
|
+
|
92
|
+
def content_type(new_type)
|
93
|
+
@content_type = Rack::Mime.mime_type('.%s' % new_type)
|
94
|
+
end
|
95
|
+
|
96
|
+
def status(status_code)
|
97
|
+
@status = status_code.to_i
|
98
|
+
end
|
99
|
+
|
100
|
+
def halt(rack_status, rack_headers, rack_body)
|
101
|
+
throw :halt, [rack_status, rack_headers, rack_body]
|
102
|
+
end
|
103
|
+
|
104
|
+
def perform_action_block(&blk)
|
105
|
+
# Execut the action in a Sinatra-like fashion - passing the route parameter values as
|
106
|
+
# arguments to the given block/callable. This is where in the future we should ditch
|
107
|
+
# the Sinatra calling conventions - Sinatra mandates that the action accept the route parameters
|
108
|
+
# as arguments and grab all the useful stuff from instance methods like `params` etc. whereas
|
109
|
+
# we probably want to have just Rack apps mounted per route (under an action)
|
110
|
+
response = catch(:halt) do
|
111
|
+
body_string_or_rack_triplet = instance_exec(*@route_params.values, &blk)
|
112
|
+
|
113
|
+
if rack_triplet?(body_string_or_rack_triplet)
|
114
|
+
return body_string_or_rack_triplet
|
115
|
+
end
|
116
|
+
|
117
|
+
[@status, {'Content-Type' => @content_type}, [body_string_or_rack_triplet]]
|
118
|
+
end
|
119
|
+
|
120
|
+
return response
|
121
|
+
end
|
122
|
+
|
123
|
+
def rack_triplet?(maybe_triplet)
|
124
|
+
maybe_triplet.is_a?(Array) &&
|
125
|
+
maybe_triplet.length == 3 &&
|
126
|
+
maybe_triplet[0].is_a?(Integer) &&
|
127
|
+
maybe_triplet[1].is_a?(Hash) &&
|
128
|
+
maybe_triplet[1].keys.all? {|k| k.is_a?(String) } &&
|
129
|
+
maybe_triplet[2].respond_to?(:each)
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Apiculture
|
3
|
+
# A poor man's ActiveSupport::HashWithIndifferentAccess, with all the Rails-y
|
4
|
+
# stuff removed.
|
5
|
+
#
|
6
|
+
# Implements a hash where keys <tt>:foo</tt> and <tt>"foo"</tt> are
|
7
|
+
# considered to be the same.
|
8
|
+
#
|
9
|
+
# rgb = Sinatra::IndifferentHash.new
|
10
|
+
#
|
11
|
+
# rgb[:black] = '#000000' # symbol assignment
|
12
|
+
# rgb[:black] # => '#000000' # symbol retrieval
|
13
|
+
# rgb['black'] # => '#000000' # string retrieval
|
14
|
+
#
|
15
|
+
# rgb['white'] = '#FFFFFF' # string assignment
|
16
|
+
# rgb[:white] # => '#FFFFFF' # symbol retrieval
|
17
|
+
# rgb['white'] # => '#FFFFFF' # string retrieval
|
18
|
+
#
|
19
|
+
# Internally, symbols are mapped to strings when used as keys in the entire
|
20
|
+
# writing interface (calling e.g. <tt>[]=</tt>, <tt>merge</tt>). This mapping
|
21
|
+
# belongs to the public interface. For example, given:
|
22
|
+
#
|
23
|
+
# hash = Sinatra::IndifferentHash.new(:a=>1)
|
24
|
+
#
|
25
|
+
# You are guaranteed that the key is returned as a string:
|
26
|
+
#
|
27
|
+
# hash.keys # => ["a"]
|
28
|
+
#
|
29
|
+
# Technically other types of keys are accepted:
|
30
|
+
#
|
31
|
+
# hash = Sinatra::IndifferentHash.new(:a=>1)
|
32
|
+
# hash[0] = 0
|
33
|
+
# hash # => { "a"=>1, 0=>0 }
|
34
|
+
#
|
35
|
+
# But this class is intended for use cases where strings or symbols are the
|
36
|
+
# expected keys and it is convenient to understand both as the same. For
|
37
|
+
# example the +params+ hash in Sinatra.
|
38
|
+
class IndifferentHash < Hash
|
39
|
+
def self.[](*args)
|
40
|
+
new.merge!(Hash[*args])
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(*args)
|
44
|
+
super(*args.map(&method(:convert_value)))
|
45
|
+
end
|
46
|
+
|
47
|
+
def default(*args)
|
48
|
+
super(*args.map(&method(:convert_key)))
|
49
|
+
end
|
50
|
+
|
51
|
+
def default=(value)
|
52
|
+
super(convert_value(value))
|
53
|
+
end
|
54
|
+
|
55
|
+
def assoc(key)
|
56
|
+
super(convert_key(key))
|
57
|
+
end
|
58
|
+
|
59
|
+
def rassoc(value)
|
60
|
+
super(convert_value(value))
|
61
|
+
end
|
62
|
+
|
63
|
+
def fetch(key, *args)
|
64
|
+
super(convert_key(key), *args.map(&method(:convert_value)))
|
65
|
+
end
|
66
|
+
|
67
|
+
def [](key)
|
68
|
+
super(convert_key(key))
|
69
|
+
end
|
70
|
+
|
71
|
+
def []=(key, value)
|
72
|
+
super(convert_key(key), convert_value(value))
|
73
|
+
end
|
74
|
+
|
75
|
+
alias_method :store, :[]=
|
76
|
+
|
77
|
+
def key(value)
|
78
|
+
super(convert_value(value))
|
79
|
+
end
|
80
|
+
|
81
|
+
def key?(key)
|
82
|
+
super(convert_key(key))
|
83
|
+
end
|
84
|
+
|
85
|
+
alias_method :has_key?, :key?
|
86
|
+
alias_method :include?, :key?
|
87
|
+
alias_method :member?, :key?
|
88
|
+
|
89
|
+
def value?(value)
|
90
|
+
super(convert_value(value))
|
91
|
+
end
|
92
|
+
|
93
|
+
alias_method :has_value?, :value?
|
94
|
+
|
95
|
+
def delete(key)
|
96
|
+
super(convert_key(key))
|
97
|
+
end
|
98
|
+
|
99
|
+
def dig(key, *other_keys)
|
100
|
+
super(convert_key(key), *other_keys)
|
101
|
+
end if method_defined?(:dig) # Added in Ruby 2.3
|
102
|
+
|
103
|
+
def fetch_values(*keys)
|
104
|
+
super(*keys.map(&method(:convert_key)))
|
105
|
+
end if method_defined?(:fetch_values) # Added in Ruby 2.3
|
106
|
+
|
107
|
+
def slice(*keys)
|
108
|
+
keys.map!(&method(:convert_key))
|
109
|
+
self.class[super(*keys)]
|
110
|
+
end if method_defined?(:slice) # Added in Ruby 2.5
|
111
|
+
|
112
|
+
def values_at(*keys)
|
113
|
+
super(*keys.map(&method(:convert_key)))
|
114
|
+
end
|
115
|
+
|
116
|
+
def merge!(other_hash)
|
117
|
+
return super if other_hash.is_a?(self.class)
|
118
|
+
|
119
|
+
other_hash.each_pair do |key, value|
|
120
|
+
key = convert_key(key)
|
121
|
+
value = yield(key, self[key], value) if block_given? && key?(key)
|
122
|
+
self[key] = convert_value(value)
|
123
|
+
end
|
124
|
+
|
125
|
+
self
|
126
|
+
end
|
127
|
+
|
128
|
+
alias_method :update, :merge!
|
129
|
+
|
130
|
+
def merge(other_hash, &block)
|
131
|
+
dup.merge!(other_hash, &block)
|
132
|
+
end
|
133
|
+
|
134
|
+
def replace(other_hash)
|
135
|
+
super(other_hash.is_a?(self.class) ? other_hash : self.class[other_hash])
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def convert_key(key)
|
141
|
+
key.is_a?(Symbol) ? key.to_s : key
|
142
|
+
end
|
143
|
+
|
144
|
+
def convert_value(value)
|
145
|
+
case value
|
146
|
+
when Hash
|
147
|
+
value.is_a?(self.class) ? value : self.class[value]
|
148
|
+
when Array
|
149
|
+
value.map(&method(:convert_value))
|
150
|
+
else
|
151
|
+
value
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -21,15 +21,14 @@ module Apiculture::SinatraInstanceMethods
|
|
21
21
|
def json_halt(with_error_message, status: 400, **attrs_for_json_response)
|
22
22
|
# Pretty-print + newline to be terminal-friendly
|
23
23
|
err_str = JSON.pretty_generate({error: with_error_message}.merge(attrs_for_json_response)) + NEWLINE
|
24
|
-
halt status, {'Content-Type' => 'application/json'}, [err_str]
|
24
|
+
halt status, {'Content-Type' => 'application/json'}, [err_str]
|
25
25
|
end
|
26
26
|
|
27
27
|
# Handles the given action via the given class, passing it the instance variables
|
28
28
|
# given in the keyword arguments
|
29
29
|
def action_result(action_class, **action_ivars)
|
30
30
|
call_result = action_class.new(self, **action_ivars).perform
|
31
|
-
|
32
|
-
unless call_result.is_a?(Array) || call_result.is_a?(Hash) || (call_result.nil? && status == 204)
|
31
|
+
unless call_result.is_a?(Array) || call_result.is_a?(Hash) || (call_result.nil? && @status == 204)
|
33
32
|
raise "Action result should be an Array, a Hash or it can be nil but only if status is 204, instead it was a #{call_result.class}"
|
34
33
|
end
|
35
34
|
|
data/lib/apiculture/version.rb
CHANGED
data/lib/apiculture.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# Allows brief definitions of APIs for documentation and parameter checks
|
2
2
|
module Apiculture
|
3
3
|
require_relative 'apiculture/version'
|
4
|
+
require_relative 'apiculture/indifferent_hash'
|
5
|
+
require_relative 'apiculture/app'
|
4
6
|
require_relative 'apiculture/action'
|
5
7
|
require_relative 'apiculture/sinatra_instance_methods'
|
6
8
|
require_relative 'apiculture/action_definition'
|
@@ -4,42 +4,42 @@ describe Apiculture::Action do
|
|
4
4
|
context '.new' do
|
5
5
|
it 'exposes the methods of the object given as a first argument to initialize' do
|
6
6
|
action_class = Class.new(described_class)
|
7
|
-
|
8
|
-
action = action_class.new(
|
7
|
+
fake_app = double('Apiculture::App', something: 'value')
|
8
|
+
action = action_class.new(fake_app)
|
9
9
|
expect(action).to respond_to(:something)
|
10
10
|
expect(action.something).to eq('value')
|
11
11
|
end
|
12
|
-
|
12
|
+
|
13
13
|
it 'converts keyword arguments to instance variables' do
|
14
14
|
action_class = Class.new(described_class)
|
15
15
|
action = action_class.new(nil, foo: 'a string')
|
16
16
|
expect(action.instance_variable_get('@foo')).to eq('a string')
|
17
17
|
end
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
it 'responds to perform()' do
|
21
21
|
expect(described_class.new(nil)).to respond_to(:perform)
|
22
22
|
end
|
23
|
-
|
23
|
+
|
24
24
|
it 'can use bail() to throw a Sinatra halt' do
|
25
|
-
|
26
|
-
expect(
|
25
|
+
fake_app = double('Apiculture::App')
|
26
|
+
expect(fake_app).to receive(:json_halt).with('Failure', status: 400)
|
27
27
|
action_class = Class.new(described_class)
|
28
|
-
action_class.new(
|
28
|
+
action_class.new(fake_app).bail "Failure"
|
29
29
|
end
|
30
|
-
|
30
|
+
|
31
31
|
it 'can use bail() to throw a Sinatra halt with a custom status' do
|
32
|
-
|
33
|
-
expect(
|
34
|
-
|
32
|
+
fake_app = double('Apiculture::App')
|
33
|
+
expect(fake_app).to receive(:json_halt).with("Failure", status: 417)
|
34
|
+
|
35
35
|
action_class = Class.new(described_class)
|
36
|
-
action_class.new(
|
36
|
+
action_class.new(fake_app).bail "Failure", status: 417
|
37
37
|
end
|
38
|
-
|
38
|
+
|
39
39
|
it 'can use bail() to throw a Sinatra halt with extra JSON attributes' do
|
40
|
-
|
41
|
-
expect(
|
40
|
+
fake_app = double('Apiculture::App')
|
41
|
+
expect(fake_app).to receive(:json_halt).with("Failure", status: 417, message: "Totale")
|
42
42
|
action_class = Class.new(described_class)
|
43
|
-
action_class.new(
|
43
|
+
action_class.new(fake_app).bail "Failure", status: 417, message: 'Totale'
|
44
44
|
end
|
45
45
|
end
|
@@ -2,7 +2,7 @@ require_relative '../spec_helper'
|
|
2
2
|
|
3
3
|
describe "Apiculture.api_documentation" do
|
4
4
|
let(:app) {
|
5
|
-
Class.new(
|
5
|
+
Class.new(Apiculture::App) do
|
6
6
|
extend Apiculture
|
7
7
|
|
8
8
|
markdown_string 'This API is very important. Because it has to do with pancakes.'
|
@@ -71,7 +71,7 @@ describe "Apiculture.api_documentation" do
|
|
71
71
|
end
|
72
72
|
|
73
73
|
it 'generates app documentation honoring the mount point' do
|
74
|
-
overridden = Class.new(
|
74
|
+
overridden = Class.new(Apiculture::App) do
|
75
75
|
extend Apiculture
|
76
76
|
mounted_at '/api/v2/'
|
77
77
|
api_method :get, '/pancakes' do
|
@@ -83,7 +83,7 @@ describe "Apiculture.api_documentation" do
|
|
83
83
|
end
|
84
84
|
|
85
85
|
it 'generates app documentation injecting the inline Markdown strings' do
|
86
|
-
app_class = Class.new(
|
86
|
+
app_class = Class.new(Apiculture::App) do
|
87
87
|
extend Apiculture
|
88
88
|
markdown_string '# This describes important stuff'
|
89
89
|
api_method :get, '/pancakes' do
|
@@ -103,7 +103,7 @@ describe "Apiculture.api_documentation" do
|
|
103
103
|
before(:each) { File.open('./TEST.md', 'w') {|f| f << "# This is an important header"} }
|
104
104
|
after(:each) { File.unlink('./TEST.md') }
|
105
105
|
it 'splices the contents of the file using markdown_file' do
|
106
|
-
app_class = Class.new(
|
106
|
+
app_class = Class.new(Apiculture::App) do
|
107
107
|
extend Apiculture
|
108
108
|
markdown_file './TEST.md'
|
109
109
|
api_method :get, '/pancakes' do
|
data/spec/apiculture_spec.rb
CHANGED
@@ -2,71 +2,69 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
|
2
2
|
|
3
3
|
describe "Apiculture" do
|
4
4
|
include Rack::Test::Methods
|
5
|
-
|
5
|
+
|
6
6
|
before(:each) { @app_class = nil }
|
7
7
|
def app
|
8
8
|
@app_class or raise "No @app_class defined in the example"
|
9
9
|
end
|
10
|
-
|
10
|
+
|
11
11
|
context 'as API definition DSL' do
|
12
12
|
it 'allows all the standard Siantra DSL to go through without modifications' do
|
13
|
-
@app_class = Class.new(
|
14
|
-
settings.show_exceptions = false
|
15
|
-
settings.raise_errors = true
|
13
|
+
@app_class = Class.new(Apiculture::App) do
|
16
14
|
extend Apiculture
|
17
|
-
|
15
|
+
|
18
16
|
post '/things/*' do
|
19
|
-
|
17
|
+
params.inspect
|
20
18
|
end
|
21
19
|
end
|
22
|
-
|
20
|
+
|
23
21
|
post '/things/a/b/c/d', {'foo' => 'bar'}
|
24
22
|
expect(last_response.body).to eq("{\"foo\"=>\"bar\", \"splat\"=>[\"a/b/c/d\"], \"captures\"=>[\"a/b/c/d\"]}")
|
25
23
|
end
|
26
|
-
|
24
|
+
|
27
25
|
it 'flags :captures as a reserved Sinatra parameter when used as a URL param' do
|
28
26
|
expect {
|
29
|
-
Class.new(
|
27
|
+
Class.new(Apiculture::App) do
|
30
28
|
extend Apiculture
|
31
29
|
route_param :captures, "Something it captures"
|
32
30
|
api_method(:get, '/thing/:captures') { raise "Should never be called" }
|
33
31
|
end
|
34
32
|
}.to raise_error(/\:captures is a reserved magic parameter name/)
|
35
33
|
end
|
36
|
-
|
34
|
+
|
37
35
|
it 'flags :captures as a reserved Sinatra parameter when used as a request param' do
|
38
36
|
expect {
|
39
|
-
Class.new(
|
37
|
+
Class.new(Apiculture::App) do
|
40
38
|
extend Apiculture
|
41
39
|
param :captures, "Something it captures", String
|
42
40
|
api_method(:get, '/thing') { raise "Should never be called" }
|
43
41
|
end
|
44
42
|
}.to raise_error(/\:captures is a reserved magic parameter name/)
|
45
43
|
end
|
46
|
-
|
44
|
+
|
47
45
|
it 'flags :splat as a reserved Sinatra parameter when used as a URL param' do
|
48
46
|
expect {
|
49
|
-
Class.new(
|
47
|
+
Class.new(Apiculture::App) do
|
50
48
|
extend Apiculture
|
51
49
|
route_param :splat, "Something it splats"
|
52
50
|
api_method(:get, '/thing/:splat') { raise "Should never be called" }
|
53
51
|
end
|
54
52
|
}.to raise_error(/\:splat is a reserved magic parameter name/)
|
55
53
|
end
|
56
|
-
|
54
|
+
|
57
55
|
it 'flags :splat as a reserved Sinatra parameter when used as a request param' do
|
58
56
|
expect {
|
59
|
-
Class.new(
|
57
|
+
Class.new(Apiculture::App) do
|
60
58
|
extend Apiculture
|
61
59
|
param :splat, "Something it splats", String
|
62
60
|
api_method(:get, '/thing') { raise "Should never be called" }
|
63
61
|
end
|
64
62
|
}.to raise_error(/\:splat is a reserved magic parameter name/)
|
65
63
|
end
|
66
|
-
|
64
|
+
|
67
65
|
it 'flags URL and request params of the same name' do
|
68
66
|
expect {
|
69
|
-
Class.new(
|
67
|
+
Class.new(Apiculture::App) do
|
70
68
|
extend Apiculture
|
71
69
|
route_param :id, 'Id of the thing'
|
72
70
|
param :id, "Something it identifies (conflict)", String
|
@@ -74,15 +72,12 @@ describe "Apiculture" do
|
|
74
72
|
end
|
75
73
|
}.to raise_error(/\:id mentioned twice/)
|
76
74
|
end
|
77
|
-
|
75
|
+
|
78
76
|
it "defines a basic API that can be called" do
|
79
77
|
$created_thing = nil
|
80
|
-
@app_class = Class.new(
|
81
|
-
settings.show_exceptions = false
|
82
|
-
settings.raise_errors = true
|
83
|
-
|
78
|
+
@app_class = Class.new(Apiculture::App) do
|
84
79
|
extend Apiculture
|
85
|
-
|
80
|
+
|
86
81
|
desc "Create a Thing with a name"
|
87
82
|
route_param :id, "The ID of the thing"
|
88
83
|
required_param :name, "Name of the thing", String
|
@@ -91,60 +86,53 @@ describe "Apiculture" do
|
|
91
86
|
'Wild success'
|
92
87
|
end
|
93
88
|
end
|
94
|
-
|
89
|
+
|
95
90
|
post '/thing/123', {name: 'Monsieur Thing'}
|
96
91
|
expect(last_response.body).to eq('Wild success')
|
97
92
|
expect($created_thing).to eq({id: '123', name: 'Monsieur Thing'})
|
98
93
|
end
|
99
|
-
|
94
|
+
|
100
95
|
it "serves the API documentation at a given URL using serve_api_documentation_at" do
|
101
96
|
$created_thing = nil
|
102
|
-
@app_class = Class.new(
|
103
|
-
settings.show_exceptions = false
|
104
|
-
settings.raise_errors = true
|
105
|
-
|
97
|
+
@app_class = Class.new(Apiculture::App) do
|
106
98
|
extend Apiculture
|
107
|
-
|
99
|
+
|
108
100
|
desc "Create a Thing with a name"
|
109
101
|
required_param :name, "Name of the thing", String
|
110
102
|
api_method( :post, '/thing/:id') {}
|
111
103
|
serve_api_documentation_at('/documentation')
|
112
104
|
end
|
113
|
-
|
105
|
+
|
114
106
|
get '/documentation'
|
115
107
|
expect(last_response['Content-Type']).to include('text/html')
|
116
108
|
expect(last_response.body).to include('Create a Thing')
|
117
109
|
end
|
118
|
-
|
110
|
+
|
119
111
|
it 'raises when a required param is not provided' do
|
120
|
-
@app_class = Class.new(
|
121
|
-
settings.show_exceptions = false
|
122
|
-
settings.raise_errors = true
|
112
|
+
@app_class = Class.new(Apiculture::App) do
|
123
113
|
extend Apiculture
|
124
|
-
|
114
|
+
|
125
115
|
required_param :name, "Name of the thing", String
|
126
116
|
api_method :post, '/thing' do
|
127
117
|
raise "Should never be called"
|
128
118
|
end
|
129
119
|
end
|
130
|
-
|
120
|
+
|
131
121
|
expect {
|
132
122
|
post '/thing', {}
|
133
123
|
}.to raise_error('Missing parameter :name')
|
134
124
|
end
|
135
|
-
|
125
|
+
|
136
126
|
it 'verifies the parameter type' do
|
137
|
-
@app_class = Class.new(
|
138
|
-
settings.show_exceptions = false
|
139
|
-
settings.raise_errors = true
|
127
|
+
@app_class = Class.new(Apiculture::App) do
|
140
128
|
extend Apiculture
|
141
|
-
|
129
|
+
|
142
130
|
required_param :number, "Number of the thing", Integer
|
143
131
|
api_method :post, '/thing' do
|
144
132
|
raise "Should never be called"
|
145
133
|
end
|
146
134
|
end
|
147
|
-
|
135
|
+
|
148
136
|
expect {
|
149
137
|
post '/thing', {number: '123'}
|
150
138
|
}.to raise_error('Received String, expected Integer for :number')
|
@@ -156,12 +144,10 @@ describe "Apiculture" do
|
|
156
144
|
value == "Magic word"
|
157
145
|
end
|
158
146
|
end.new
|
159
|
-
|
160
|
-
@app_class = Class.new(
|
161
|
-
settings.show_exceptions = false
|
162
|
-
settings.raise_errors = true
|
147
|
+
|
148
|
+
@app_class = Class.new(Apiculture::App) do
|
163
149
|
extend Apiculture
|
164
|
-
|
150
|
+
|
165
151
|
required_param :pretty_please, "Only a magic word will do", custom_matcher
|
166
152
|
api_method :post, '/thing' do
|
167
153
|
'Ohai!'
|
@@ -177,25 +163,21 @@ describe "Apiculture" do
|
|
177
163
|
end
|
178
164
|
|
179
165
|
it 'suppresses parameters that are not defined in the action definition' do
|
180
|
-
@app_class = Class.new(
|
181
|
-
settings.show_exceptions = false
|
182
|
-
settings.raise_errors = true
|
166
|
+
@app_class = Class.new(Apiculture::App) do
|
183
167
|
extend Apiculture
|
184
|
-
|
168
|
+
|
185
169
|
api_method :post, '/thing' do
|
186
170
|
raise ":evil_ssh_injection should have wiped from params{}" if params[:evil_ssh_injection]
|
187
171
|
'All is well'
|
188
172
|
end
|
189
173
|
end
|
190
|
-
|
174
|
+
|
191
175
|
post '/thing', {evil_ssh_injection: 'I am Homakov!'}
|
192
176
|
expect(last_response).to be_ok
|
193
177
|
end
|
194
178
|
|
195
179
|
it 'allows route parameters that are not mentioned in the action definition, but are given in Sinatra path' do
|
196
|
-
@app_class = Class.new(
|
197
|
-
settings.show_exceptions = false
|
198
|
-
settings.raise_errors = true
|
180
|
+
@app_class = Class.new(Apiculture::App) do
|
199
181
|
extend Apiculture
|
200
182
|
|
201
183
|
api_method :post, '/api-thing/:id_of_thing' do |id|
|
@@ -214,7 +196,7 @@ describe "Apiculture" do
|
|
214
196
|
'All is well'
|
215
197
|
end
|
216
198
|
end
|
217
|
-
|
199
|
+
|
218
200
|
post '/vanilla-thing/123456'
|
219
201
|
expect(last_response).to be_ok
|
220
202
|
|
@@ -223,9 +205,7 @@ describe "Apiculture" do
|
|
223
205
|
end
|
224
206
|
|
225
207
|
it 'does not clobber the status set in a separate mutating call when using json_response' do
|
226
|
-
@app_class = Class.new(
|
227
|
-
settings.show_exceptions = false
|
228
|
-
settings.raise_errors = true
|
208
|
+
@app_class = Class.new(Apiculture::App) do
|
229
209
|
extend Apiculture
|
230
210
|
|
231
211
|
api_method :post, '/api/:id' do
|
@@ -233,27 +213,38 @@ describe "Apiculture" do
|
|
233
213
|
json_response({was_created: true})
|
234
214
|
end
|
235
215
|
end
|
236
|
-
|
216
|
+
|
237
217
|
post '/api/123'
|
238
218
|
expect(last_response.status).to eq(201)
|
239
219
|
end
|
240
|
-
|
220
|
+
|
241
221
|
it 'raises when describing a route parameter that is not included in the path' do
|
242
222
|
expect {
|
243
|
-
Class.new(
|
223
|
+
Class.new(Apiculture::App) do
|
244
224
|
extend Apiculture
|
245
225
|
route_param :thing_id, "The ID of the thing"
|
246
226
|
api_method(:get, '/thing/:id') { raise "Should never be called" }
|
247
227
|
end
|
248
228
|
}.to raise_error('Parameter :thing_id not present in path "/thing/:id"')
|
249
229
|
end
|
250
|
-
|
230
|
+
|
231
|
+
it 'returns a 404 when a non existing route is called' do
|
232
|
+
@app_class = Class.new(Apiculture::App) do
|
233
|
+
extend Apiculture
|
234
|
+
|
235
|
+
api_method :post, '/api' do
|
236
|
+
[1]
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
post '/api-404'
|
241
|
+
expect(last_response.status).to eq(404)
|
242
|
+
end
|
243
|
+
|
251
244
|
it 'applies a symbol typecast by calling a method on the parameter value' do
|
252
|
-
@app_class = Class.new(
|
253
|
-
settings.show_exceptions = false
|
254
|
-
settings.raise_errors = true
|
245
|
+
@app_class = Class.new(Apiculture::App) do
|
255
246
|
extend Apiculture
|
256
|
-
|
247
|
+
|
257
248
|
required_param :number, "Number of the thing", Integer, :cast => :to_i
|
258
249
|
api_method :post, '/thing' do
|
259
250
|
raise "Not cast" unless params[:number] == 123
|
@@ -265,11 +256,9 @@ describe "Apiculture" do
|
|
265
256
|
end
|
266
257
|
|
267
258
|
it 'ensures current behaviour for route params is not changed' do
|
268
|
-
@app_class = Class.new(
|
269
|
-
settings.show_exceptions = false
|
270
|
-
settings.raise_errors = true
|
259
|
+
@app_class = Class.new(Apiculture::App) do
|
271
260
|
extend Apiculture
|
272
|
-
|
261
|
+
|
273
262
|
route_param :number, "Number of the thing"
|
274
263
|
api_method :post, '/thing/:number' do
|
275
264
|
raise "Casted to int" if params[:number] == 123
|
@@ -280,12 +269,22 @@ describe "Apiculture" do
|
|
280
269
|
expect(last_response.body).to eq('Total success')
|
281
270
|
end
|
282
271
|
|
272
|
+
it 'supports returning a rack triplet' do
|
273
|
+
@app_class = Class.new(Apiculture::App) do
|
274
|
+
extend Apiculture
|
275
|
+
api_method :get, '/rack' do
|
276
|
+
[402, {'X-Money-In-The-Bank' => 'yes, please'}, ['Buy bitcoin']]
|
277
|
+
end
|
278
|
+
end
|
279
|
+
get '/rack'
|
280
|
+
expect(last_response.status).to eq 402
|
281
|
+
expect(last_response.body).to eq 'Buy bitcoin'
|
282
|
+
end
|
283
|
+
|
283
284
|
it 'ensures current behaviour when no route params are present does not change' do
|
284
|
-
@app_class = Class.new(
|
285
|
-
settings.show_exceptions = false
|
286
|
-
settings.raise_errors = true
|
285
|
+
@app_class = Class.new(Apiculture::App) do
|
287
286
|
extend Apiculture
|
288
|
-
|
287
|
+
|
289
288
|
param :number, "Number of the thing", Integer, cast: :to_i
|
290
289
|
api_method :post, '/thing' do
|
291
290
|
raise "Behaviour changed" unless params[:number] == 123
|
@@ -297,11 +296,9 @@ describe "Apiculture" do
|
|
297
296
|
end
|
298
297
|
|
299
298
|
it 'applies a symbol typecast by calling a method on the route parameter value' do
|
300
|
-
@app_class = Class.new(
|
301
|
-
settings.show_exceptions = false
|
302
|
-
settings.raise_errors = true
|
299
|
+
@app_class = Class.new(Apiculture::App) do
|
303
300
|
extend Apiculture
|
304
|
-
|
301
|
+
|
305
302
|
route_param :number, "Number of the thing", Integer, :cast => :to_i
|
306
303
|
api_method :post, '/thing/:number' do
|
307
304
|
raise "Not cast" unless params[:number] == 123
|
@@ -314,28 +311,29 @@ describe "Apiculture" do
|
|
314
311
|
|
315
312
|
|
316
313
|
it 'cast block arguments to the right type', run: true do
|
317
|
-
@app_class = Class.new(
|
318
|
-
settings.show_exceptions = false
|
319
|
-
settings.raise_errors = true
|
314
|
+
@app_class = Class.new(Apiculture::App) do
|
320
315
|
extend Apiculture
|
321
|
-
|
316
|
+
|
322
317
|
route_param :number, "Number of the thing", Integer, :cast => :to_i
|
323
318
|
api_method :post, '/thing/:number' do |number|
|
324
|
-
raise "Not cast" unless number.
|
319
|
+
raise "Not cast" unless number.is_a?(Integer)
|
325
320
|
'Total success'
|
326
321
|
end
|
327
322
|
end
|
328
323
|
post '/thing/123'
|
329
324
|
expect(last_response.body).to eq('Total success')
|
325
|
+
|
326
|
+
# Double checking that bignums are okay, too
|
327
|
+
bignum = 10**30
|
328
|
+
post "/thing/#{bignum}"
|
329
|
+
expect(last_response.body).to eq('Total success')
|
330
330
|
end
|
331
331
|
|
332
|
-
|
332
|
+
|
333
333
|
it 'merges route_params and regular params' do
|
334
|
-
@app_class = Class.new(
|
335
|
-
settings.show_exceptions = false
|
336
|
-
settings.raise_errors = true
|
334
|
+
@app_class = Class.new(Apiculture::App) do
|
337
335
|
extend Apiculture
|
338
|
-
|
336
|
+
|
339
337
|
param :number, "Number of the thing", Integer, :cast => :to_i
|
340
338
|
route_param :id, "Id of the thingy", Integer, :cast => :to_i
|
341
339
|
route_param :awesome, "Hash of the thingy"
|
@@ -352,11 +350,9 @@ describe "Apiculture" do
|
|
352
350
|
|
353
351
|
|
354
352
|
it 'applies a Proc typecast by calling the proc (for example - for ISO8601 time)' do
|
355
|
-
@app_class = Class.new(
|
356
|
-
settings.show_exceptions = false
|
357
|
-
settings.raise_errors = true
|
353
|
+
@app_class = Class.new(Apiculture::App) do
|
358
354
|
extend Apiculture
|
359
|
-
|
355
|
+
|
360
356
|
required_param :when, "When it happened", Time, cast: ->(v){ Time.parse(v) }
|
361
357
|
api_method :post, '/occurrence' do
|
362
358
|
raise "Not cast" unless params[:when].year == 2015
|
@@ -368,18 +364,16 @@ describe "Apiculture" do
|
|
368
364
|
expect(last_response.body).to eq('Total success')
|
369
365
|
end
|
370
366
|
end
|
371
|
-
|
367
|
+
|
372
368
|
context 'Sinatra instance method extensions' do
|
373
369
|
it 'adds support for json_response' do
|
374
|
-
@app_class = Class.new(
|
370
|
+
@app_class = Class.new(Apiculture::App) do
|
375
371
|
extend Apiculture
|
376
|
-
settings.show_exceptions = false
|
377
|
-
settings.raise_errors = true
|
378
372
|
api_method :get, '/some-json' do
|
379
373
|
json_response({foo: 'bar'})
|
380
374
|
end
|
381
375
|
end
|
382
|
-
|
376
|
+
|
383
377
|
get '/some-json'
|
384
378
|
expect(last_response).to be_ok
|
385
379
|
expect(last_response['Content-Type']).to include('application/json')
|
@@ -388,24 +382,20 @@ describe "Apiculture" do
|
|
388
382
|
end
|
389
383
|
|
390
384
|
it 'adds support for json_response to set http status code', run: true do
|
391
|
-
@app_class = Class.new(
|
385
|
+
@app_class = Class.new(Apiculture::App) do
|
392
386
|
extend Apiculture
|
393
|
-
settings.show_exceptions = false
|
394
|
-
settings.raise_errors = true
|
395
387
|
api_method :post, '/some-json' do
|
396
388
|
json_response({foo: 'bar'}, status: 201)
|
397
389
|
end
|
398
390
|
end
|
399
|
-
|
391
|
+
|
400
392
|
post '/some-json'
|
401
393
|
expect(last_response.status).to eq(201)
|
402
394
|
end
|
403
395
|
|
404
396
|
it 'adds support for json_halt' do
|
405
|
-
@app_class = Class.new(
|
397
|
+
@app_class = Class.new(Apiculture::App) do
|
406
398
|
extend Apiculture
|
407
|
-
settings.show_exceptions = false
|
408
|
-
settings.raise_errors = true
|
409
399
|
api_method :get, '/simple-halt' do
|
410
400
|
json_halt "Nein."
|
411
401
|
raise "This should never be called"
|
@@ -419,13 +409,13 @@ describe "Apiculture" do
|
|
419
409
|
raise "This should never be called"
|
420
410
|
end
|
421
411
|
end
|
422
|
-
|
412
|
+
|
423
413
|
get '/simple-halt'
|
424
414
|
expect(last_response.status).to eq(400)
|
425
415
|
expect(last_response['Content-Type']).to include('application/json')
|
426
416
|
parsed_body = JSON.load(last_response.body)
|
427
417
|
expect(parsed_body).to eq({"error"=>"Nein."})
|
428
|
-
|
418
|
+
|
429
419
|
get '/halt-with-error-payload'
|
430
420
|
expect(last_response.status).to eq(400)
|
431
421
|
expect(last_response['Content-Type']).to include('application/json')
|
@@ -441,10 +431,8 @@ describe "Apiculture" do
|
|
441
431
|
end
|
442
432
|
end
|
443
433
|
it 'allows returning an empty body when the status is 204' do
|
444
|
-
@app_class = Class.new(
|
434
|
+
@app_class = Class.new(Apiculture::App) do
|
445
435
|
extend Apiculture
|
446
|
-
settings.show_exceptions = false
|
447
|
-
settings.raise_errors = true
|
448
436
|
api_method :get, '/nil204' do
|
449
437
|
action_result NilTestAction
|
450
438
|
end
|
@@ -458,10 +446,8 @@ describe "Apiculture" do
|
|
458
446
|
it "does not allow returning an empty body when the status isn't 204" do
|
459
447
|
# Mock out the perform call so that status doesn't change from the default of 200
|
460
448
|
expect_any_instance_of(NilTestAction).to receive(:perform).with(any_args).and_return(nil)
|
461
|
-
@app_class = Class.new(
|
449
|
+
@app_class = Class.new(Apiculture::App) do
|
462
450
|
extend Apiculture
|
463
|
-
settings.show_exceptions = false
|
464
|
-
settings.raise_errors = true
|
465
451
|
api_method :get, '/nil200' do
|
466
452
|
action_result NilTestAction
|
467
453
|
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: apiculture
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
@@ -9,10 +9,10 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2018-05-28 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
|
-
name:
|
15
|
+
name: mustermann
|
16
16
|
requirement: !ruby/object:Gem::Requirement
|
17
17
|
requirements:
|
18
18
|
- - "~>"
|
@@ -121,14 +121,14 @@ dependencies:
|
|
121
121
|
requirements:
|
122
122
|
- - "~>"
|
123
123
|
- !ruby/object:Gem::Version
|
124
|
-
version: '
|
124
|
+
version: '6.0'
|
125
125
|
type: :development
|
126
126
|
prerelease: false
|
127
127
|
version_requirements: !ruby/object:Gem::Requirement
|
128
128
|
requirements:
|
129
129
|
- - "~>"
|
130
130
|
- !ruby/object:Gem::Version
|
131
|
-
version: '
|
131
|
+
version: '6.0'
|
132
132
|
- !ruby/object:Gem::Dependency
|
133
133
|
name: rake
|
134
134
|
requirement: !ruby/object:Gem::Requirement
|
@@ -171,7 +171,7 @@ dependencies:
|
|
171
171
|
- - ">="
|
172
172
|
- !ruby/object:Gem::Version
|
173
173
|
version: '0'
|
174
|
-
description: A toolkit for building REST APIs on top of
|
174
|
+
description: A toolkit for building REST APIs on top of Rack
|
175
175
|
email: me@julik.nl
|
176
176
|
executables: []
|
177
177
|
extensions: []
|
@@ -186,11 +186,15 @@ files:
|
|
186
186
|
- README.md
|
187
187
|
- Rakefile
|
188
188
|
- apiculture.gemspec
|
189
|
+
- gemfiles/Gemfile.rack-1.x
|
190
|
+
- gemfiles/Gemfile.rack-2.x
|
189
191
|
- lib/apiculture.rb
|
190
192
|
- lib/apiculture/action.rb
|
191
193
|
- lib/apiculture/action_definition.rb
|
194
|
+
- lib/apiculture/app.rb
|
192
195
|
- lib/apiculture/app_documentation.rb
|
193
196
|
- lib/apiculture/app_documentation_tpl.mustache
|
197
|
+
- lib/apiculture/indifferent_hash.rb
|
194
198
|
- lib/apiculture/markdown_segment.rb
|
195
199
|
- lib/apiculture/method_documentation.rb
|
196
200
|
- lib/apiculture/sinatra_instance_methods.rb
|
@@ -222,8 +226,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
222
226
|
version: '0'
|
223
227
|
requirements: []
|
224
228
|
rubyforge_project:
|
225
|
-
rubygems_version: 2.6.
|
229
|
+
rubygems_version: 2.6.14.1
|
226
230
|
signing_key:
|
227
231
|
specification_version: 4
|
228
|
-
summary: Sweet API sauce on top of
|
232
|
+
summary: Sweet API sauce on top of Rack
|
229
233
|
test_files: []
|