apiculture 0.0.13 → 0.0.14

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 110ceee2209d9f8ed34b76754b273a2d0834ac40
4
- data.tar.gz: 6b4c9cad319d4c3cb1fb9cc2055634e09635fc7d
3
+ metadata.gz: 52cb357c6a73e99a64db5e12a108b7a73cd5afd8
4
+ data.tar.gz: 7c03623922d85c6947ca1ca4026d3b29e584fe4f
5
5
  SHA512:
6
- metadata.gz: c3a192e792b99422ff9fdb13dec002e083d044b5fd391187967ecd6a8982c2d0067fa03dfd1749406b8ebd573c7de715a1621969ba562c402de0c439ef1ce662
7
- data.tar.gz: 32b19da637ed027e4f01dd4e03c3334da9cdcd1509aea9d4a360b2520cf3b860284cce7e2293a6a65ce67712d2e6471e87228b900a5ff1c7351d2b5fe74b23bc
6
+ metadata.gz: 9c97d38dab84b5c6d4cda3d2fb04c30f1c4c3ea8f7ff6cdd6432f5656ddc6523a7a95aaa4919324f28839669e8aa3c7bea0e65c0c73ab274573625805c1bd3e5
7
+ data.tar.gz: 8d289be328fbc019a55dab97317c219d0ddb48e181078bc446d5da4d13b8b6de2accf688d03a6965bd8733928efb71f633916ad60a4f6d9900f38c375e0ed26d
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ rvm:
2
+ - 2.1.5
3
+ - 2.2.2
4
+ sudo: false
5
+ cache: bundler
data/Gemfile CHANGED
@@ -10,6 +10,6 @@ group :development do
10
10
  gem "rspec", "~> 3.1", '< 3.2'
11
11
  gem "rdoc", "~> 3.12"
12
12
  gem "bundler", "~> 1.0"
13
- gem "jeweler", "~> 2.0.1"
13
+ gem "jeweler", "~> 2"
14
14
  gem "simplecov", ">= 0"
15
15
  end
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  A little toolkit for building RESTful API backends on top of Sinatra.
4
4
 
5
+ [![Build Status](https://travis-ci.org/WeTransfer/apiculture.svg?branch=master)](https://travis-ci.org/WeTransfer/apiculture)
6
+
5
7
  ## Ideas
6
8
 
7
9
  A simple API definition DSL with simple premises:
@@ -15,54 +17,60 @@ A simple API definition DSL with simple premises:
15
17
 
16
18
  ## A taste of honey
17
19
 
18
- class Api::V2 < Sinatra::Base
19
-
20
- use Rack::Parser, :content_types => {
21
- 'application/json' => JSON.method(:load).to_proc
22
- }
23
-
24
- extend Apiculture
25
-
26
- desc 'Create a Contact'
27
- required_param :name, 'Name of the person', String
28
- param :email, 'Email address of the person', String
29
- param :phone, 'Phone number', String, cast: ->(v) { v.scan(/\d/).flatten.join }
30
- param :notes, 'Notes about this person', String
31
- api_method :post, '/contacts' do
32
- # anything allowed within Sinatra actions is allowed here, and
33
- # works exactly the same - but we suggest using Actions instead.
34
- action_result CreateContact # uses Api::V2::CreateContact
35
- end
36
-
37
- desc 'Fetch a Contact'
38
- route_param :id, 'ID of the person'
39
- responds_with 200, 'Contact data', {name: 'John Appleseed', id: "ac19...fefg"}
40
- api_method :get, '/contacts/:id' do | person_id |
41
- json Person.find(person_id).to_json
42
- end
43
- end
20
+ ```ruby
21
+ class Api::V2 < Sinatra::Base
22
+
23
+ use Rack::Parser, :content_types => {
24
+ 'application/json' => JSON.method(:load).to_proc
25
+ }
26
+
27
+ extend Apiculture
28
+
29
+ desc 'Create a Contact'
30
+ required_param :name, 'Name of the person', String
31
+ param :email, 'Email address of the person', String
32
+ param :phone, 'Phone number', String, cast: ->(v) { v.scan(/\d/).flatten.join }
33
+ param :notes, 'Notes about this person', String
34
+ api_method :post, '/contacts' do
35
+ # anything allowed within Sinatra actions is allowed here, and
36
+ # works exactly the same - but we suggest using Actions instead.
37
+ action_result CreateContact # uses Api::V2::CreateContact
38
+ end
39
+
40
+ desc 'Fetch a Contact'
41
+ route_param :id, 'ID of the person'
42
+ responds_with 200, 'Contact data', {name: 'John Appleseed', id: "ac19...fefg"}
43
+ api_method :get, '/contacts/:id' do | person_id |
44
+ json Person.find(person_id).to_json
45
+ end
46
+ end
47
+ ```
44
48
 
45
49
  ## Generating documentation
46
50
 
47
51
  For the aforementioned example:
48
52
 
49
- File.open('API.html', 'w') do |f|
50
- f << Api::V2.api_documentation.to_html
51
- end
53
+ ```ruby
54
+ File.open('API.html', 'w') do |f|
55
+ f << Api::V2.api_documentation.to_html
56
+ end
57
+ ```
52
58
 
53
59
  or to get it in Markdown:
54
60
 
55
- File.open('API.md', 'w') do |f|
56
- f << Api::V2.api_documentation.to_markdown
57
- end
61
+ ```ruby
62
+ File.open('API.md', 'w') do |f|
63
+ f << Api::V2.api_documentation.to_markdown
64
+ end
65
+ ```
58
66
 
59
67
  ## Running the tests
60
68
 
61
- $bundle exec rspec
69
+ $ bundle exec rspec
62
70
 
63
71
  If you want to also examine the HTML documentation that gets built during the test, set `SHOW_TEST_DOC` in env:
64
72
 
65
- $SHOW_TEST_DOC=yes bundle exec rspec
73
+ $ SHOW_TEST_DOC=yes bundle exec rspec
66
74
 
67
75
  Note that this requires presence of the `open` commandline utility (should be available on both OSX and Linux).
68
76
 
data/apiculture.gemspec CHANGED
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: apiculture 0.0.13 ruby lib
5
+ # stub: apiculture 0.0.14 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "apiculture"
9
- s.version = "0.0.13"
9
+ s.version = "0.0.14"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib"]
13
13
  s.authors = ["Julik Tarkhanov", "WeTransfer"]
14
- s.date = "2015-11-16"
14
+ s.date = "2016-06-01"
15
15
  s.description = "A toolkit for building REST APIs on top of Sinatra"
16
16
  s.email = "me@julik.nl"
17
17
  s.extra_rdoc_files = [
@@ -19,6 +19,7 @@ Gem::Specification.new do |s|
19
19
  "README.md"
20
20
  ]
21
21
  s.files = [
22
+ ".travis.yml",
22
23
  "Gemfile",
23
24
  "LICENSE.txt",
24
25
  "README.md",
@@ -58,7 +59,7 @@ Gem::Specification.new do |s|
58
59
  s.add_development_dependency(%q<rspec>, ["< 3.2", "~> 3.1"])
59
60
  s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
60
61
  s.add_development_dependency(%q<bundler>, ["~> 1.0"])
61
- s.add_development_dependency(%q<jeweler>, ["~> 2.0.1"])
62
+ s.add_development_dependency(%q<jeweler>, ["~> 2"])
62
63
  s.add_development_dependency(%q<simplecov>, [">= 0"])
63
64
  else
64
65
  s.add_dependency(%q<sinatra>, ["~> 1.4"])
@@ -70,7 +71,7 @@ Gem::Specification.new do |s|
70
71
  s.add_dependency(%q<rspec>, ["< 3.2", "~> 3.1"])
71
72
  s.add_dependency(%q<rdoc>, ["~> 3.12"])
72
73
  s.add_dependency(%q<bundler>, ["~> 1.0"])
73
- s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
74
+ s.add_dependency(%q<jeweler>, ["~> 2"])
74
75
  s.add_dependency(%q<simplecov>, [">= 0"])
75
76
  end
76
77
  else
@@ -83,7 +84,7 @@ Gem::Specification.new do |s|
83
84
  s.add_dependency(%q<rspec>, ["< 3.2", "~> 3.1"])
84
85
  s.add_dependency(%q<rdoc>, ["~> 3.12"])
85
86
  s.add_dependency(%q<bundler>, ["~> 1.0"])
86
- s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
87
+ s.add_dependency(%q<jeweler>, ["~> 2"])
87
88
  s.add_dependency(%q<simplecov>, [">= 0"])
88
89
  end
89
90
  end
@@ -11,7 +11,7 @@ class Apiculture::MethodDocumentation
11
11
  @definition = action_definition
12
12
  @mountpoint = mountpoint
13
13
  end
14
-
14
+
15
15
  # Compose a Markdown definition of the action
16
16
  def to_markdown
17
17
  m = MDBuf.new
@@ -20,42 +20,42 @@ class Apiculture::MethodDocumentation
20
20
  m << route_parameters_table
21
21
  m << request_parameters_table
22
22
  m << possible_responses_table
23
-
23
+
24
24
  m.to_s
25
25
  end
26
-
26
+
27
27
  # Compose an HTML string by converting the result of +to_markdown+
28
28
  def to_html_fragment
29
29
  require 'rdiscount'
30
30
  RDiscount.new(to_markdown).to_html
31
31
  end
32
-
32
+
33
33
  private
34
-
34
+
35
35
  class StringBuf #:nodoc:
36
36
  def initialize; @blocks = []; end
37
37
  def <<(block); @blocks << block.to_s; self; end
38
38
  def to_s; @blocks.join; end
39
39
  end
40
-
40
+
41
41
  class MDBuf < StringBuf #:nodoc:
42
42
  def to_s; @blocks.join("\n\n"); end
43
43
  end
44
-
45
- def route_parameters_table
44
+
45
+ def _route_parameters_table
46
46
  return '' unless @definition.defines_route_params?
47
-
47
+
48
48
  m = MDBuf.new
49
49
  b = StringBuf.new
50
50
  m << '### URL parameters'
51
-
51
+
52
52
  html = Builder::XmlMarkup.new(:target => b)
53
53
  html.table(class: 'apiculture-table') do
54
54
  html.tr do
55
55
  html.th 'Name'
56
56
  html.th 'Description'
57
57
  end
58
-
58
+
59
59
  @definition.route_parameters.each do | param |
60
60
  html.tr do
61
61
  html.td { html.tt(':%s' % param.name) }
@@ -65,7 +65,7 @@ class Apiculture::MethodDocumentation
65
65
  end
66
66
  m << b.to_s
67
67
  end
68
-
68
+
69
69
  def body_example(for_response_definition)
70
70
  if for_response_definition.no_body?
71
71
  '(empty)'
@@ -73,14 +73,14 @@ class Apiculture::MethodDocumentation
73
73
  JSON.pretty_generate(for_response_definition.jsonable_object_example)
74
74
  end
75
75
  end
76
-
76
+
77
77
  def possible_responses_table
78
78
  return '' unless @definition.defines_responses?
79
-
79
+
80
80
  m = MDBuf.new
81
81
  b = StringBuf.new
82
82
  m << '### Possible responses'
83
-
83
+
84
84
  html = Builder::XmlMarkup.new(:target => b)
85
85
  html.table(class: 'apiculture-table') do
86
86
  html.tr do
@@ -88,7 +88,7 @@ class Apiculture::MethodDocumentation
88
88
  html.th('What happened')
89
89
  html.th('Example response body')
90
90
  end
91
-
91
+
92
92
  @definition.responses.each do | resp |
93
93
  html.tr do
94
94
  html.td { html.b(resp.http_status_code) }
@@ -97,15 +97,27 @@ class Apiculture::MethodDocumentation
97
97
  end
98
98
  end
99
99
  end
100
-
100
+
101
101
  m << b.to_s
102
102
  end
103
-
103
+
104
104
  def request_parameters_table
105
105
  return '' unless @definition.defines_request_params?
106
-
107
106
  m = MDBuf.new
108
107
  m << '### Request parameters'
108
+ m << parameters_table(@definition.parameters).to_s
109
+ end
110
+
111
+ def route_parameters_table
112
+ return '' unless @definition.defines_route_params?
113
+ m = MDBuf.new
114
+ m << '### URL parameters'
115
+ m << parameters_table(@definition.route_parameters).to_s
116
+ end
117
+
118
+
119
+ private
120
+ def parameters_table(parameters)
109
121
  b = StringBuf.new
110
122
  html = Builder::XmlMarkup.new(:target => b)
111
123
  html.table(class: 'apiculture-table') do
@@ -115,8 +127,8 @@ class Apiculture::MethodDocumentation
115
127
  html.th 'Type after cast'
116
128
  html.th 'Description'
117
129
  end
118
-
119
- @definition.parameters.each do | param |
130
+
131
+ parameters.each do | param |
120
132
  html.tr do
121
133
  html.td { html.tt(param.name.to_s) }
122
134
  html.td(param.required ? 'Yes' : 'No')
@@ -125,6 +137,6 @@ class Apiculture::MethodDocumentation
125
137
  end
126
138
  end
127
139
  end
128
- m << b
140
+ b
129
141
  end
130
142
  end
@@ -1,3 +1,3 @@
1
1
  module Apiculture
2
- VERSION = '0.0.13'
2
+ VERSION = '0.0.14'
3
3
  end
data/lib/apiculture.rb CHANGED
@@ -35,8 +35,7 @@ module Apiculture
35
35
  def name_as_string; name.to_s; end
36
36
  end
37
37
 
38
- class RouteParameter < Struct.new(:name, :description)
39
- def name_as_string; name.to_s; end
38
+ class RouteParameter < Parameter
40
39
  end
41
40
 
42
41
  class PossibleResponse < Struct.new(:http_status_code, :description, :jsonable_object_example)
@@ -119,9 +118,9 @@ module Apiculture
119
118
  # Route parameters are always required, and all the parameters specified
120
119
  # using +route_param+ should also be included in the path given for the route
121
120
  # definition
122
- def route_param(name, description)
121
+ def route_param(name, description, ruby_type = String, cast: IDENTITY_PROC)
123
122
  @apiculture_action_definition ||= ActionDefinition.new
124
- @apiculture_action_definition.route_parameters << RouteParameter.new(name, description)
123
+ @apiculture_action_definition.route_parameters << RouteParameter.new(name, description, required=false, ruby_type, cast)
125
124
  end
126
125
 
127
126
  # Add a possible response, specifying the code and the JSON Response by example.
@@ -245,12 +244,22 @@ module Apiculture
245
244
  # Pick out all the defined parameters and set up a block that can validate them
246
245
  # when the action is called. With that, set up the actual Sinatra method that will
247
246
  # respond to the request.
248
- parametric_checker_proc = parametric_validator_proc_from(action_def.parameters)
247
+ parametric_checker_proc = parametric_validator_proc_from(action_def.parameters + action_def.route_parameters)
249
248
  public_send(http_verb, path, options) do |*matched_sinatra_route_params|
250
- # Verify the parameters first
249
+ route_params = []
250
+ action_def.route_parameters.each_with_index do |route_param, index|
251
+ # Apply the type cast and save it (since using our override we can mutate the params)
252
+ value_after_type_cast = AC_APPLY_TYPECAST_PROC.call(route_param.cast_proc_or_method, params[route_param.name])
253
+ route_params[index] = value_after_type_cast
254
+
255
+ # Ensure the typecast value adheres to the enforced Ruby type
256
+ AC_CHECK_TYPE_PROC.call(route_param, route_params[index])
257
+ # ..permit it in the strong parameters if we support them
258
+ AC_PERMIT_PROC.call(route_params, route_param.name)
259
+ end
251
260
  instance_exec(&parametric_checker_proc)
252
261
  # Execute the original action via instance_exec, passing along the route args
253
- instance_exec(*matched_sinatra_route_params, &blk)
262
+ instance_exec(*route_params, &blk)
254
263
  end
255
264
 
256
265
  # Reset for the subsequent action definition
@@ -27,6 +27,11 @@ describe "Apiculture.api_documentation" do
27
27
  route_param :id, 'Pancake ID to delete'
28
28
  api_method :delete, '/pancake/:id' do
29
29
  end
30
+
31
+ desc 'Pancake ingredients are in the URL'
32
+ route_param :topping_id, 'Pancake topping ID', Fixnum, cast: :to_i
33
+ api_method :get, '/pancake/with/:topping_id' do |topping_id|
34
+ end
30
35
  end
31
36
  }
32
37
 
@@ -63,6 +63,24 @@ describe Apiculture::MethodDocumentation do
63
63
  expect(generated_html).not_to include('<h3>Request parameters</h3>')
64
64
  end
65
65
 
66
+ it 'generates HTML from an ActionDefinition with a casted route param' do
67
+ definition = Apiculture::ActionDefinition.new
68
+
69
+ definition.description = "This adds a topping to a pancake"
70
+
71
+ definition.route_parameters << Apiculture::RouteParameter.new(:topping_id, 'ID of the pancake topping', Fixnum, cast: :to_i)
72
+ definition.http_verb = 'get'
73
+ definition.path = '/pancake/:topping_id'
74
+
75
+ documenter = described_class.new(definition)
76
+
77
+ generated_html = documenter.to_html_fragment
78
+ generated_markdown = documenter.to_markdown
79
+ expect(generated_html).to include('<h3>URL parameters</h3>')
80
+ expect(generated_html).to include('Type after cast')
81
+ end
82
+
83
+
66
84
  it 'generates Markdown from an ActionDefinition with a mountpoint' do
67
85
  definition = Apiculture::ActionDefinition.new
68
86
 
@@ -191,7 +191,94 @@ describe "Apiculture" do
191
191
  post '/thing', {number: '123'}
192
192
  expect(last_response.body).to eq('Total success')
193
193
  end
194
+
195
+ it 'ensures current behaviour for route params is not changed' do
196
+ @app_class = Class.new(Sinatra::Base) do
197
+ settings.show_exceptions = false
198
+ settings.raise_errors = true
199
+ extend Apiculture
200
+
201
+ route_param :number, "Number of the thing"
202
+ api_method :post, '/thing/:number' do
203
+ raise "Casted to int" if params[:number] == 123
204
+ 'Total success'
205
+ end
206
+ end
207
+ post '/thing/123'
208
+ expect(last_response.body).to eq('Total success')
209
+ end
210
+
211
+ it 'ensures current behaviour when no route params are present does not change' do
212
+ @app_class = Class.new(Sinatra::Base) do
213
+ settings.show_exceptions = false
214
+ settings.raise_errors = true
215
+ extend Apiculture
216
+
217
+ param :number, "Number of the thing", Integer, cast: :to_i
218
+ api_method :post, '/thing' do
219
+ raise "Behaviour changed" unless params[:number] == 123
220
+ 'Total success'
221
+ end
222
+ end
223
+ post '/thing', {number: '123'}
224
+ expect(last_response.body).to eq('Total success')
225
+ end
226
+
227
+ it 'applies a symbol typecast by calling a method on the route parameter value' do
228
+ @app_class = Class.new(Sinatra::Base) do
229
+ settings.show_exceptions = false
230
+ settings.raise_errors = true
231
+ extend Apiculture
232
+
233
+ route_param :number, "Number of the thing", Integer, :cast => :to_i
234
+ api_method :post, '/thing/:number' do
235
+ raise "Not cast" unless params[:number] == 123
236
+ 'Total success'
237
+ end
238
+ end
239
+ post '/thing/123'
240
+ expect(last_response.body).to eq('Total success')
241
+ end
242
+
243
+
244
+ it 'cast block arguments to the right type', run: true do
245
+ @app_class = Class.new(Sinatra::Base) do
246
+ settings.show_exceptions = false
247
+ settings.raise_errors = true
248
+ extend Apiculture
249
+
250
+ route_param :number, "Number of the thing", Fixnum, :cast => :to_i
251
+ api_method :post, '/thing/:number' do |number|
252
+ raise "Not cast" unless number.class == Fixnum
253
+ 'Total success'
254
+ end
255
+ end
256
+ post '/thing/123'
257
+ expect(last_response.body).to eq('Total success')
258
+ end
259
+
194
260
 
261
+ it 'merges route_params and regular params' do
262
+ @app_class = Class.new(Sinatra::Base) do
263
+ settings.show_exceptions = false
264
+ settings.raise_errors = true
265
+ extend Apiculture
266
+
267
+ param :number, "Number of the thing", Integer, :cast => :to_i
268
+ route_param :id, "Id of the thingy", Fixnum, :cast => :to_i
269
+ route_param :awesome, "Hash of the thingy"
270
+
271
+ api_method :post, '/thing/:id/:awesome' do |id|
272
+ raise 'Not merged' unless params.has_key?("id")
273
+ raise 'Not merged' unless params.has_key?("awesome")
274
+ 'Thanks'
275
+ end
276
+ end
277
+ post '/thing/1/true', {number: '123'}
278
+ expect(last_response.body).to eq('Thanks')
279
+ end
280
+
281
+
195
282
  it 'applies a Proc typecast by calling the proc (for example - for ISO8601 time)' do
196
283
  @app_class = Class.new(Sinatra::Base) do
197
284
  settings.show_exceptions = false
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.13
4
+ version: 0.0.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julik Tarkhanov
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-11-16 00:00:00.000000000 Z
12
+ date: 2016-06-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sinatra
@@ -149,14 +149,14 @@ dependencies:
149
149
  requirements:
150
150
  - - "~>"
151
151
  - !ruby/object:Gem::Version
152
- version: 2.0.1
152
+ version: '2'
153
153
  type: :development
154
154
  prerelease: false
155
155
  version_requirements: !ruby/object:Gem::Requirement
156
156
  requirements:
157
157
  - - "~>"
158
158
  - !ruby/object:Gem::Version
159
- version: 2.0.1
159
+ version: '2'
160
160
  - !ruby/object:Gem::Dependency
161
161
  name: simplecov
162
162
  requirement: !ruby/object:Gem::Requirement
@@ -179,6 +179,7 @@ extra_rdoc_files:
179
179
  - LICENSE.txt
180
180
  - README.md
181
181
  files:
182
+ - ".travis.yml"
182
183
  - Gemfile
183
184
  - LICENSE.txt
184
185
  - README.md