apiculture 0.1.7 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 21e9fada9f8ba88c058f19789d597059bb30b055fe92c10e27953c5df6296758
4
- data.tar.gz: 7b3c02a5587b6976a2a1a7b11656834824e32887b9dde3922542687fcbe63c71
3
+ metadata.gz: d78cc86595f72f0ec4ea08adaa5b545798eb54653aafedb47d3e6923351b3b76
4
+ data.tar.gz: 532aa3794d65dfd962d4d5984a29678e026b5da43efd1f5ad5dbaea4d4ee6ff1
5
5
  SHA512:
6
- metadata.gz: b0818c0fd6c04efa47f0cabcd0927c13516bd293d7486fea8750b7325d260208b3134432ebf43fbc9d7f0321a8ff7b8e5ea07e6f679e59b1df62e0f5f105f1b7
7
- data.tar.gz: 853ca12aefe6005ae08432d29f902f6a1584252db87663c08c96a00f47d7b771108e2485f39e7400615e8f54243f68f6abf2e8406a963def5e3821c9ee62a6fc
6
+ metadata.gz: b351b52e4a10c1c7ee2b7d4f7b5c8f63d4ed8b28f65348ebf63ec0f064ccb55b9bfd6cad6f21d49c34867436dfc50036e67f4d8fdb206f3525581346ee7db3a8
7
+ data.tar.gz: 9a0d97711023461335d4237f44e077daf2b1669c9509bb53dcd79571fb8a157188fb06b9d93db28ab9df69f170d152c50746ce8c27f73eb2af7781963d8e26bf
data/.gitignore CHANGED
@@ -1,7 +1,3 @@
1
- # rcov generated
2
- coverage
3
- coverage.data
4
-
5
1
  # rdoc generated
6
2
  rdoc
7
3
 
@@ -16,7 +12,7 @@ Gemfile.lock
16
12
  # jeweler generated
17
13
  pkg
18
14
 
19
- # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
15
+ # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
20
16
  #
21
17
  # * Create a file at ~/.gitignore
22
18
  # * Include files you want ignored
@@ -48,3 +44,5 @@ pkg
48
44
 
49
45
  # For rubinius:
50
46
  #*.rbc
47
+
48
+ .ruby-version
data/.travis.yml CHANGED
@@ -7,5 +7,7 @@ rvm:
7
7
  - 2.5
8
8
  - 2.6
9
9
  - 2.7
10
+ - 3.0
11
+ - 3.2
10
12
  sudo: false
11
13
  cache: bundler
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## 0.2.0
2
+ * Add ruby 3 support
3
+ * Bump `mustermann` dependency to '~> 3'
data/apiculture.gemspec CHANGED
@@ -4,14 +4,13 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'apiculture/version'
5
5
 
6
6
  Gem::Specification.new do |s|
7
- s.name = "apiculture"
7
+ s.name = 'apiculture'
8
8
  s.version = Apiculture::VERSION
9
9
 
10
- s.require_paths = ["lib"]
11
- s.authors = ["Julik Tarkhanov", "WeTransfer"]
12
- s.homepage = 'http://github.com/wetransfer/zip_tricks'
13
- s.description = "A toolkit for building REST APIs on top of Rack"
14
- s.email = "me@julik.nl"
10
+ s.require_paths = ['lib']
11
+ s.authors = ['Julik Tarkhanov', 'WeTransfer']
12
+ s.description = 'A toolkit for building REST APIs on top of Rack'
13
+ s.email = 'me@julik.nl'
15
14
 
16
15
  # Prevent pushing this gem to RubyGems.org.
17
16
  # To allow pushes either set the 'allowed_push_host'
@@ -23,26 +22,26 @@ Gem::Specification.new do |s|
23
22
  'public gem pushes.'
24
23
  end
25
24
 
26
- s.files = `git ls-files -z`.split("\x0")
25
+ s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^spec/}) }
27
26
  s.extra_rdoc_files = [
28
- "LICENSE.txt",
29
- "README.md"
27
+ 'LICENSE.txt',
28
+ 'README.md'
30
29
  ]
31
- s.homepage = "https://github.com/WeTransfer/apiculture"
32
- s.licenses = ["MIT"]
33
- s.rubygems_version = "2.4.5.1"
34
- s.summary = "Sweet API sauce on top of Rack"
30
+ s.homepage = 'https://github.com/WeTransfer/apiculture'
31
+ s.licenses = ['MIT']
32
+ s.rubygems_version = '2.4.5.1'
33
+ s.summary = 'Sweet API sauce on top of Rack'
35
34
 
36
- s.add_runtime_dependency 'mustermann', '~> 1'
37
35
  s.add_runtime_dependency 'builder', '~> 3'
38
- s.add_runtime_dependency 'rdiscount', '~> 2'
39
36
  s.add_runtime_dependency 'github-markup', '~> 3'
40
37
  s.add_runtime_dependency 'mustache', '~> 1'
38
+ s.add_runtime_dependency 'mustermann', '~> 3'
39
+ s.add_runtime_dependency 'rdiscount', '~> 2'
41
40
 
41
+ s.add_development_dependency 'bundler', '~> 2.0'
42
+ s.add_development_dependency 'cgi'
42
43
  s.add_development_dependency 'rack-test'
43
- s.add_development_dependency "rspec", "~> 3"
44
- s.add_development_dependency "rdoc", "~> 6.0"
45
- s.add_development_dependency "rake"
46
- s.add_development_dependency "bundler", "~> 1.0"
47
- s.add_development_dependency "simplecov", ">= 0"
44
+ s.add_development_dependency 'rake'
45
+ s.add_development_dependency 'rdoc', '~> 6.0'
46
+ s.add_development_dependency 'rspec', '~> 3'
48
47
  end
@@ -9,9 +9,8 @@ gem 'mustache', '~> 1'
9
9
 
10
10
  group :development do
11
11
  gem 'rack-test'
12
- gem "rspec", "~> 3.1", '< 3.2'
12
+ gem "rspec", "~> 3"
13
13
  gem "rdoc", "~> 6.0"
14
- gem "rake", "~> 10"
15
- gem "simplecov", ">= 0"
14
+ gem "rake"
16
15
  gem "bundler"
17
16
  end
@@ -9,9 +9,8 @@ gem 'mustache', '~> 1'
9
9
 
10
10
  group :development do
11
11
  gem 'rack-test'
12
- gem "rspec", "~> 3.1", '< 3.2'
12
+ gem "rspec", "~> 3"
13
13
  gem "rdoc", "~> 6.0"
14
- gem "rake", "~> 10"
15
- gem "simplecov", ">= 0"
14
+ gem "rake"
16
15
  gem "bundler"
17
16
  end
@@ -13,19 +13,19 @@ class Apiculture::App
13
13
  end
14
14
 
15
15
  def get(url, **options, &handler_blk)
16
- define_action :get, url, options, &handler_blk
16
+ define_action :get, url, **options, &handler_blk
17
17
  end
18
18
 
19
19
  def post(url, **options, &handler_blk)
20
- define_action :post, url, options, &handler_blk
20
+ define_action :post, url, **options, &handler_blk
21
21
  end
22
22
 
23
23
  def put(url, **options, &handler_blk)
24
- define_action :put, url, options, &handler_blk
24
+ define_action :put, url, **options, &handler_blk
25
25
  end
26
26
 
27
27
  def delete(url, **options, &handler_blk)
28
- define_action :delete, url, options, &handler_blk
28
+ define_action :delete, url, **options, &handler_blk
29
29
  end
30
30
 
31
31
  def actions
@@ -4,7 +4,7 @@ require 'json'
4
4
  module Apiculture::SinatraInstanceMethods
5
5
  NEWLINE = "\n"
6
6
  DEFAULT = :__default__
7
-
7
+
8
8
  # Convert the given structure to JSON, set the content-type and
9
9
  # return the JSON string
10
10
  def json_response(structure, status: DEFAULT)
@@ -12,7 +12,7 @@ module Apiculture::SinatraInstanceMethods
12
12
  status(status) unless status == DEFAULT
13
13
  JSON.pretty_generate(structure)
14
14
  end
15
-
15
+
16
16
  # Bail out from an action by sending a halt() via Sinatra. Is most useful for
17
17
  # handling access denied, invalid resource and other types of situations
18
18
  # where you don't want the request to continue, but still would like to
@@ -20,10 +20,10 @@ module Apiculture::SinatraInstanceMethods
20
20
  # with it's own JSON means.
21
21
  def json_halt(with_error_message, status: 400, **attrs_for_json_response)
22
22
  # Pretty-print + newline to be terminal-friendly
23
- err_str = JSON.pretty_generate({error: with_error_message}.merge(attrs_for_json_response)) + NEWLINE
23
+ err_str = JSON.pretty_generate({error: with_error_message}.merge(**attrs_for_json_response)) + NEWLINE
24
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)
@@ -31,7 +31,7 @@ module Apiculture::SinatraInstanceMethods
31
31
  unless call_result.is_a?(Array) || call_result.is_a?(Hash) || (call_result.nil? && @status == 204)
32
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}"
33
33
  end
34
-
34
+
35
35
  json_response call_result if call_result
36
36
  end
37
37
  end
@@ -1,3 +1,3 @@
1
1
  module Apiculture
2
- VERSION = '0.1.7'
2
+ VERSION = '0.2.0'.freeze
3
3
  end
data/lib/apiculture.rb CHANGED
@@ -10,40 +10,40 @@ module Apiculture
10
10
  require_relative 'apiculture/timestamp_promise'
11
11
  require_relative 'apiculture/app_documentation'
12
12
  require_relative 'apiculture/openapi_documentation'
13
-
13
+
14
14
  def self.extended(in_class)
15
15
  in_class.send(:include, SinatraInstanceMethods)
16
16
  super
17
17
  end
18
-
18
+
19
19
  IDENTITY_PROC = ->(arg) { arg }
20
-
20
+
21
21
  AC_APPLY_TYPECAST_PROC = ->(cast_proc_or_method, v) {
22
22
  cast_proc_or_method.is_a?(Symbol) ? v.public_send(cast_proc_or_method) : cast_proc_or_method.call(v)
23
23
  }
24
-
24
+
25
25
  AC_CHECK_PRESENCE_PROC = ->(name_as_string, params) {
26
26
  params.has_key?(name_as_string) or raise MissingParameter.new(name_as_string)
27
27
  }
28
-
28
+
29
29
  AC_CHECK_TYPE_PROC = ->(param, value) {
30
30
  param.matchable === value or raise ParameterTypeMismatch.new(param, value.class)
31
31
  }
32
-
32
+
33
33
  class Parameter < Struct.new(:name, :description, :required, :matchable, :cast_proc_or_method)
34
34
  # Return Strings since Sinatra prefers string keys for params{}
35
35
  def name_as_string; name.to_s; end
36
36
  end
37
-
37
+
38
38
  class RouteParameter < Parameter
39
39
  end
40
-
40
+
41
41
  class PossibleResponse < Struct.new(:http_status_code, :description, :jsonable_object_example)
42
42
  def no_body?
43
43
  jsonable_object_example.nil?
44
44
  end
45
45
  end
46
-
46
+
47
47
  # Indicates where this API will be mounted. This is only used
48
48
  # for the generated documentation. In general, this should match
49
49
  # the SCRIPT_NAME of the Sinatra application when it will be called.
@@ -61,13 +61,13 @@ module Apiculture
61
61
  def mounted_at(path)
62
62
  @apiculture_mounted_at = path.to_s.gsub(/\/$/, '')
63
63
  end
64
-
64
+
65
65
  # Inserts the generation timestamp into the documentation at this point.
66
66
  # The timestamp will be not very precise (to the minute) and in UTC time
67
67
  def documentation_build_time!
68
68
  apiculture_stack << Apiculture::TimestampPromise
69
69
  end
70
-
70
+
71
71
  # Inserts a literal Markdown string into the documentation at this point.
72
72
  # For instance, if used after an API method declaration, it will insert
73
73
  # the header between the API methods in the doc.
@@ -82,7 +82,7 @@ module Apiculture
82
82
  def markdown_string(str)
83
83
  apiculture_stack << MarkdownSegment.new(str)
84
84
  end
85
-
85
+
86
86
  # Inserts the contents of the file at +path+ into the documentation, using +markdown_string+.
87
87
  # For instance, if used after an API method declaration, it will insert
88
88
  # the header between the API methods in the doc.
@@ -95,25 +95,25 @@ module Apiculture
95
95
  md = File.read(path_to_markdown).encode(Encoding::UTF_8)
96
96
  markdown_string(md)
97
97
  end
98
-
98
+
99
99
  # Describe the API method that is going to be defined
100
100
  def desc(action_description)
101
101
  @apiculture_action_definition ||= ActionDefinition.new
102
102
  @apiculture_action_definition.description = action_description.to_s
103
103
  end
104
-
104
+
105
105
  # Add an optional parameter for the API call
106
106
  def param(name, description, matchable, cast: IDENTITY_PROC)
107
107
  @apiculture_action_definition ||= ActionDefinition.new
108
108
  @apiculture_action_definition.parameters << Parameter.new(name, description, required=false, matchable, cast)
109
109
  end
110
-
110
+
111
111
  # Add a requred parameter for the API call
112
112
  def required_param(name, description, matchable, cast: IDENTITY_PROC)
113
113
  @apiculture_action_definition ||= ActionDefinition.new
114
114
  @apiculture_action_definition.parameters << Parameter.new(name, description, required=true, matchable, cast)
115
115
  end
116
-
116
+
117
117
  # Describe a parameter that has to be included in the URL of the API call.
118
118
  # Route parameters are always required, and all the parameters specified
119
119
  # using +route_param+ should also be included in the path given for the route
@@ -122,28 +122,28 @@ module Apiculture
122
122
  @apiculture_action_definition ||= ActionDefinition.new
123
123
  @apiculture_action_definition.route_parameters << RouteParameter.new(name, description, required=false, matchable, cast)
124
124
  end
125
-
125
+
126
126
  # Add a possible response, specifying the code and the JSON Response by example.
127
127
  # Multiple response packages can be specified.
128
128
  def responds_with(http_status, description, example_jsonable_object = nil)
129
129
  @apiculture_action_definition ||= ActionDefinition.new
130
130
  @apiculture_action_definition.responses << PossibleResponse.new(http_status, description, example_jsonable_object)
131
131
  end
132
-
132
+
133
133
  DefinitionError = Class.new(StandardError)
134
134
  ValidationError = Class.new(StandardError)
135
-
135
+
136
136
  class RouteParameterNotInPath < DefinitionError; end
137
137
  class ReservedParameter < DefinitionError; end
138
138
  class ConflictingParameter < DefinitionError; end
139
-
139
+
140
140
  # Gets raised when a parameter is missing
141
141
  class MissingParameter < ValidationError
142
142
  def initialize(parameter_name)
143
143
  super "Missing parameter :#{parameter_name}"
144
144
  end
145
145
  end
146
-
146
+
147
147
  # Gets raised when a parameter is supplied and has a wrong type
148
148
  class ParameterTypeMismatch < ValidationError
149
149
  def initialize(ac_parameter, received_ruby_type)
@@ -152,7 +152,7 @@ module Apiculture
152
152
  super "Received #{received_type}, expected #{expected_type.inspect} for :#{parameter_name}"
153
153
  end
154
154
  end
155
-
155
+
156
156
  # Returns a Proc that calls the strong parameters to check the presence/types
157
157
  def parametric_validator_proc_from(parametric_validators, implicitly_defined_route_parameter_names)
158
158
  required_params = parametric_validators.select{|e| e.required }
@@ -164,23 +164,23 @@ module Apiculture
164
164
  parametric_validators.each do |param|
165
165
  param_name = param.name_as_string
166
166
  next unless params.has_key?(param_name) # this is checked via required_params
167
-
167
+
168
168
  # Apply the type cast and save it (since using our override we can mutate the params)
169
169
  value_after_type_cast = AC_APPLY_TYPECAST_PROC.call(param.cast_proc_or_method, params[param_name])
170
170
  params[param_name] = value_after_type_cast
171
-
171
+
172
172
  # Ensure the typecast value adheres to the enforced Ruby type
173
173
  AC_CHECK_TYPE_PROC.call(param, params[param_name])
174
174
  end
175
-
176
- # The following only applies if the app does not use strong_parameters -
175
+
176
+ # The following only applies if the app does not use strong_parameters -
177
177
  # this makes use of parameter mutability again to kill the parameters that are not permitted
178
178
  # or mentioned in the API specification. We need to keep the params which are specified in the
179
179
  # route but not documented via Apiculture though
180
180
  unexpected_parameters = Set.new(params.keys.map(&:to_s)) -
181
181
  Set.new(parametric_validators.map(&:name).map(&:to_s)) -
182
182
  Set.new(implicitly_defined_route_parameter_names.map(&:to_s))
183
-
183
+
184
184
  unexpected_parameters.each do | parameter_to_discard |
185
185
  # TODO: raise or record a warning
186
186
  if env['rack.logger'].respond_to?(:warn)
@@ -190,7 +190,7 @@ module Apiculture
190
190
  end
191
191
  }
192
192
  end
193
-
193
+
194
194
  # Serve the documentation for the API at the given URL
195
195
  def serve_api_documentation_at(url)
196
196
  get(url) do
@@ -198,7 +198,7 @@ module Apiculture
198
198
  self.class.api_documentation.to_html
199
199
  end
200
200
  end
201
-
201
+
202
202
  # Returns an +AppDocumentation+ object for all actions defined so far.
203
203
  #
204
204
  # MyApi.api_documentation.to_markdown #=> "..."
@@ -213,7 +213,7 @@ module Apiculture
213
213
  action_def = (@apiculture_action_definition || ActionDefinition.new)
214
214
  action_def.http_verb = http_verb
215
215
  action_def.path = path
216
-
216
+
217
217
  # Ensure no reserved Sinatra parameters are used
218
218
  all_parameter_names = action_def.all_parameter_names_as_strings
219
219
  %w( splat captures ).each do | reserved_param |
@@ -221,34 +221,34 @@ module Apiculture
221
221
  raise ReservedParameter.new(":#{reserved_param} is a reserved magic parameter name in Sinatra")
222
222
  end
223
223
  end
224
-
224
+
225
225
  # Ensure no conflations between route/req params
226
226
  seen_params = {}
227
- all_parameter_names.each do |e|
227
+ all_parameter_names.each do |e|
228
228
  if seen_params[e]
229
- raise ConflictingParameter.new(":#{e} mentioned twice as a possible parameter. Note that URL" +
229
+ raise ConflictingParameter.new(":#{e} mentioned twice as a possible parameter. Note that URL" +
230
230
  " parameters and request parameters share a namespace.")
231
231
  else
232
232
  seen_params[e] = true
233
233
  end
234
234
  end
235
-
235
+
236
236
  # Ensure the path has the route parameters that were predeclared
237
237
  action_def.route_parameters.map(&:name).each do | route_parameter_key |
238
238
  unless path.include?(':%s' % route_parameter_key)
239
239
  raise RouteParameterNotInPath.new("Parameter :#{route_parameter_key} not present in path #{path.inspect}")
240
240
  end
241
241
  end
242
-
242
+
243
243
  # TODO: ensure all route parameters are documented
244
-
244
+
245
245
  # Pick out all the defined parameters and set up a block that can validate them
246
246
  # when the action is called. With that, set up the actual Sinatra method that will
247
247
  # respond to the request. We take care to preserve all the params that have NOT been documented
248
248
  # using Apiculture but _were_ in fact specified in the actual path.
249
249
  route_parameter_names = path.scan(/:([^:\/]+)/).flatten.map(&:to_sym)
250
250
  parametric_checker_proc = parametric_validator_proc_from(action_def.parameters + action_def.route_parameters, route_parameter_names)
251
- public_send(http_verb, path, options) do |*matched_sinatra_route_params|
251
+ public_send(http_verb, path, **options) do |*matched_sinatra_route_params|
252
252
  # Extract all the parameter names from the route path as given to the method
253
253
  route_parameters = Hash[route_parameter_names.zip(matched_sinatra_route_params)]
254
254
 
@@ -269,13 +269,13 @@ module Apiculture
269
269
  # Execute the original action via instance_exec, passing along the route args
270
270
  instance_exec(*route_parameters.values, &blk)
271
271
  end
272
-
272
+
273
273
  # Reset for the subsequent action definition
274
274
  @apiculture_action_definition = ActionDefinition.new
275
275
  # and store the just defined action for future use
276
276
  apiculture_stack << action_def
277
277
  end
278
-
278
+
279
279
  def apiculture_stack
280
280
  @apiculture_actions_and_docs ||= []
281
281
  @apiculture_actions_and_docs