apiculture 0.0.13 → 0.0.14

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 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