apiculture 0.0.19 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|
[](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: []
|