praxis 0.20.1 → 0.21

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/.travis.yml +2 -2
  4. data/CHANGELOG.md +36 -0
  5. data/lib/api_browser/Gruntfile.js +33 -3
  6. data/lib/api_browser/app/index.html +3 -0
  7. data/lib/api_browser/app/js/factories/Example.js +4 -0
  8. data/lib/api_browser/app/js/factories/template_for.js +14 -0
  9. data/lib/api_browser/app/js/filters/attribute_name.js +3 -2
  10. data/lib/api_browser/app/js/filters/header_info.js +9 -0
  11. data/lib/api_browser/app/views/action.html +11 -2
  12. data/lib/api_browser/app/views/controller.html +5 -5
  13. data/lib/api_browser/app/views/menu.html +1 -1
  14. data/lib/api_browser/app/views/type.html +4 -23
  15. data/lib/api_browser/app/views/type/details.html +7 -4
  16. data/lib/api_browser/app/views/types/main/array.html +22 -0
  17. data/lib/api_browser/app/views/types/main/default.html +23 -0
  18. data/lib/api_browser/app/views/types/main/hash.html +23 -0
  19. data/lib/praxis.rb +2 -0
  20. data/lib/praxis/action_definition.rb +0 -8
  21. data/lib/praxis/api_definition.rb +11 -6
  22. data/lib/praxis/api_general_info.rb +13 -0
  23. data/lib/praxis/bootloader.rb +11 -5
  24. data/lib/praxis/docs/generator.rb +31 -11
  25. data/lib/praxis/docs/link_builder.rb +30 -0
  26. data/lib/praxis/extensions/field_expansion.rb +2 -2
  27. data/lib/praxis/media_type.rb +1 -1
  28. data/lib/praxis/middleware_app.rb +30 -0
  29. data/lib/praxis/resource_definition.rb +24 -2
  30. data/lib/praxis/response.rb +2 -1
  31. data/lib/praxis/response_definition.rb +2 -2
  32. data/lib/praxis/responses/http.rb +28 -92
  33. data/lib/praxis/responses/validation_error.rb +4 -1
  34. data/lib/praxis/tasks/api_docs.rb +11 -24
  35. data/lib/praxis/trait.rb +12 -7
  36. data/lib/praxis/validation_handler.rb +2 -1
  37. data/lib/praxis/version.rb +1 -1
  38. data/praxis.gemspec +11 -7
  39. data/spec/api_browser/filters/attribute_name_spec.js +2 -2
  40. data/spec/praxis/action_definition_spec.rb +23 -1
  41. data/spec/praxis/bootloader_spec.rb +28 -0
  42. data/spec/praxis/extensions/field_expansion_spec.rb +10 -0
  43. data/spec/praxis/middleware_app_spec.rb +55 -0
  44. data/spec/praxis/plugins/praxis_mapper_plugin_spec.rb +8 -3
  45. data/spec/praxis/resource_definition_spec.rb +51 -2
  46. data/spec/praxis/response_definition_spec.rb +16 -4
  47. data/spec/praxis/response_spec.rb +1 -1
  48. data/spec/praxis/trait_spec.rb +13 -0
  49. data/spec/spec_app/config/environment.rb +11 -1
  50. metadata +30 -25
  51. data/lib/praxis/restful_doc_generator.rb +0 -439
@@ -78,6 +78,19 @@ module Praxis
78
78
  end
79
79
  end
80
80
 
81
+ def documentation_url(val=nil)
82
+ if val.nil?
83
+ get(:documentation_url)
84
+ else
85
+ if @global_info.nil? # this *is* the global info
86
+ set(:documentation_url, val)
87
+ else
88
+ raise "Use of documentation_url is only allowed in the global part of " \
89
+ "the API definition (but you are attempting to use it in the API " \
90
+ "definition of version #{self.version}"
91
+ end
92
+ end
93
+ end
81
94
 
82
95
  def base_path(val=nil)
83
96
  if val
@@ -93,15 +93,21 @@ module Praxis
93
93
  instance.options.merge!(options)
94
94
  instance.block = block if block_given?
95
95
 
96
- if application.plugins.key?(instance.config_key)
97
- raise "Can not use plugin: #{plugin}, another plugin is already registered with key: #{instance.config_key}"
96
+ config_key = if instance.config_key.nil?
97
+ raise "Cannot use plugin: #{plugin}. It does not have a config_key defined, and its class does not have a name" unless instance.class.name
98
+ # Default the config key based on the full class name transformed to snake case (and joining modules with '_')
99
+ instance.class.name.to_s.split('::').collect{|n| n.underscore }.join('_').to_sym
100
+ else
101
+ instance.config_key
98
102
  end
99
103
 
100
- if instance.config_key.nil?
101
- raise "Error initializing plugin: #{plugin}, config_key may not be nil."
104
+ if application.plugins.key?(instance.config_key)
105
+ used_in = application.plugins[config_key].class
106
+ raise "Can not use plugin: #{plugin}, another plugin (#{used_in}) is already registered with key: #{instance.config_key}"
102
107
  end
103
108
 
104
- application.plugins[instance.config_key] = instance
109
+ application.plugins[config_key] = instance
110
+
105
111
  instance
106
112
  end
107
113
 
@@ -79,12 +79,15 @@ module Praxis
79
79
  end
80
80
 
81
81
 
82
- # Recursively inspect the structure in data and collect any
83
- # newly discovered types into the `reachable_types` in/out parameter
84
- def collect_reachable_types( data, reachable_types )
82
+ # Data: hash/array structure of dumped resources and/or types
83
+ # processed_types: list of type classes that have already gone through a describe+collect (this or previous rounds)
84
+ # ... any processed type won't need to be described+reached any longer
85
+ # newly_found: list of type classes that have been seen in the search (and that weren't already in the processed type)
86
+ def scan_dump_for_types( data, processed_types )
87
+ newfound_types = Set.new
85
88
  case data
86
89
  when Array
87
- data.collect{|item| collect_reachable_types( item , reachable_types) }
90
+ data.collect{|item| newfound_types += scan_dump_for_types( item , processed_types ) }
88
91
  when Hash
89
92
  if data.key?(:type) && data[:type].kind_of?(Hash) && ( [:id,:name,:family] - data[:type].keys ).empty?
90
93
  type_id = data[:type][:id]
@@ -93,12 +96,12 @@ module Praxis
93
96
  raise "Error! We have detected a reference to a 'Type' with id='#{type_id}' which is not derived from Attributor::Type" +
94
97
  " Document generation cannot proceed."
95
98
  end
96
- reachable_types << types_by_id[type_id]
99
+ newfound_types << types_by_id[type_id] unless processed_types.include? types_by_id[type_id]
97
100
  end
98
101
  end
99
- data.values.map{|item| collect_reachable_types( item , reachable_types)}
102
+ data.values.map{|item| newfound_types += scan_dump_for_types( item , processed_types)}
100
103
  end
101
- reachable_types
104
+ newfound_types
102
105
  end
103
106
 
104
107
  def write_index_file( for_versions: )
@@ -124,13 +127,30 @@ module Praxis
124
127
  dumped_resources = dump_resources( resources_by_version[version] )
125
128
  found_media_types = resources_by_version[version].select{|r| r.media_type}.collect {|r| r.media_type.describe }
126
129
 
127
- collected_types = Set.new
128
- collect_reachable_types( dumped_resources, collected_types )
130
+ # We'll start by processing the rendered mediatypes
131
+ processed_types = Set.new(resources_by_version[version].select do|r|
132
+ r.media_type && !r.media_type.is_a?(Praxis::SimpleMediaType)
133
+ end.collect(&:media_type))
134
+
135
+ newfound = Set.new
129
136
  found_media_types.each do |mt|
130
- collect_reachable_types( { type: mt} , collected_types )
137
+ newfound += scan_dump_for_types( { type: mt} , processed_types )
138
+ end
139
+ # Then will process the rendered resources (noting)
140
+ newfound += scan_dump_for_types( dumped_resources, Set.new )
141
+
142
+ # At this point we've done a scan of the dumped resources and mediatypes.
143
+ # In that scan we've discovered a bunch of types, however, many of those might have appeared in the JSON
144
+ # rendered in just shallow mode, so it is not guaranteed that we've seen all the available types.
145
+ # For that we'll do a (non-shallow) dump of all the types we found, and scan them until the scans do not
146
+ # yield types we haven't seen before
147
+ while !newfound.empty? do
148
+ dumped = newfound.collect(&:describe)
149
+ processed_types += newfound
150
+ newfound = scan_dump_for_types( dumped, processed_types )
131
151
  end
132
152
 
133
- dumped_schemas = dump_schemas( collected_types )
153
+ dumped_schemas = dump_schemas( processed_types )
134
154
  full_data = {
135
155
  info: version_info[:info],
136
156
  resources: dumped_resources,
@@ -0,0 +1,30 @@
1
+ module Praxis
2
+ module Docs
3
+ # Generates links into the generated doc browser.
4
+ class LinkBuilder
5
+ include Singleton
6
+
7
+ # Generates a link based on a request gone wrong.
8
+ # @return [String, nil] The doc browser link.
9
+ def for_request(req)
10
+ build_link req.version, 'controller', req.action.resource_definition.id, req.action.name
11
+ end
12
+
13
+ private
14
+
15
+ def build_link(*segments)
16
+ if endpoint
17
+ endpoint + '#' + segments.join('/')
18
+ end
19
+ end
20
+
21
+ def endpoint
22
+ @endpoint ||= begin
23
+ endpoint = ApiDefinition.instance.global_info.documentation_url
24
+ endpoint.gsub(/\/index\.html$/i, '/') if endpoint
25
+ end
26
+ end
27
+ end
28
+
29
+ end
30
+ end
@@ -17,8 +17,8 @@ module Praxis
17
17
  extend ActiveSupport::Concern
18
18
 
19
19
  def expanded_fields(request, media_type)
20
- use_fields = self.params.attributes.key?(:fields)
21
- use_view = self.params.attributes.key?(:view)
20
+ use_fields = self.params && self.params.attributes.key?(:fields)
21
+ use_view = self.params && self.params.attributes.key?(:view)
22
22
 
23
23
  # Determine what, if any, fields to display.
24
24
  fields = if use_fields
@@ -126,7 +126,7 @@ module Praxis
126
126
  end
127
127
 
128
128
  # now to tackle whatever links there may be
129
- if (links_fields = fields[:links])
129
+ if defined?(type::Links) &&(links_fields = fields[:links])
130
130
  resolved_links = resolve_links(type::Links, links_fields)
131
131
  self.deep_merge(result, resolved_links)
132
132
  end
@@ -0,0 +1,30 @@
1
+ module Praxis
2
+ class MiddlewareApp
3
+
4
+ attr_reader :target
5
+
6
+ # Initialize the application instance with the desired args, and return the wrapping class.
7
+ def self.for( **args )
8
+ Praxis::Application.instance.setup(**args)
9
+ self
10
+ end
11
+
12
+ def initialize( inner )
13
+ @target = inner
14
+ end
15
+
16
+ def call(env)
17
+ result = Praxis::Application.instance.call(env)
18
+
19
+ unless ( [404,405].include?(result[0].to_i) && result[1]['X-Cascade'] == 'pass' )
20
+ # Respect X-Cascade header if it doesn't specify 'pass'
21
+ result
22
+ else
23
+ last_body = result[2]
24
+ last_body.close if last_body.respond_to? :close
25
+ target.call(env)
26
+ end
27
+ end
28
+
29
+ end
30
+ end
@@ -11,7 +11,8 @@ module Praxis
11
11
  @version = 'n/a'.freeze
12
12
  @actions = Hash.new
13
13
  @responses = Hash.new
14
- @action_defaults = Trait.new
14
+
15
+ @action_defaults = Trait.new &ResourceDefinition.generate_defaults_block
15
16
 
16
17
  @version_options = {}
17
18
  @metadata = {}
@@ -35,9 +36,28 @@ module Praxis
35
36
  Application.instance.resource_definitions << self
36
37
  end
37
38
 
39
+ def self.generate_defaults_block( version: nil )
40
+
41
+ # Ensure we inherit any base params defined in the API definition for the passed in version
42
+ base_attributes = if (base_params = ApiDefinition.instance.info(version).base_params)
43
+ base_params.attributes
44
+ else
45
+ {}
46
+ end
47
+
48
+ Proc.new do
49
+ unless base_attributes.empty?
50
+ params do
51
+ base_attributes.each do |base_name, base_attribute|
52
+ attribute base_name, base_attribute.type, base_attribute.options
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+
38
59
  def self.finalize!
39
60
  Application.instance.resource_definitions.each do |resource_definition|
40
-
41
61
  while (block = resource_definition.on_finalize.shift)
42
62
  block.call
43
63
  end
@@ -178,6 +198,8 @@ module Praxis
178
198
  @version_prefix = "#{Praxis::Request::path_version_prefix}#{self.version}"
179
199
  end
180
200
  end
201
+
202
+ @action_defaults.instance_eval &ResourceDefinition.generate_defaults_block( version: version )
181
203
  end
182
204
 
183
205
 
@@ -18,11 +18,12 @@ module Praxis
18
18
  klass.status = self.status if self.status
19
19
  end
20
20
 
21
- def initialize(status:self.class.status, headers:{}, body:'')
21
+ def initialize(status:self.class.status, headers:{}, body:'', location: nil)
22
22
  @name = response_name
23
23
  @status = status
24
24
  @headers = headers
25
25
  @body = body
26
+ @headers['Location'] = location if location
26
27
  @form_data = nil
27
28
  @parts = Hash.new
28
29
  end
@@ -147,14 +147,14 @@ module Praxis
147
147
  default_handlers.include?(k)
148
148
  end
149
149
 
150
- if (handler = handlers[identifier.handler_name])
150
+ if (identifier && handler = handlers[identifier.handler_name])
151
151
  payload[:examples][identifier.handler_name] = {
152
152
  content_type: identifier.to_s,
153
153
  body: handler.generate(rendered_payload)
154
154
  }
155
155
  else
156
156
  handlers.each do |name, handler|
157
- content_type = identifier + name
157
+ content_type = ( identifier ) ? identifier + name : 'application/' + name
158
158
  payload[:examples][name] = {
159
159
  content_type: content_type.to_s,
160
160
  body: handler.generate(rendered_payload)
@@ -18,11 +18,6 @@ module Praxis
18
18
  # being created.
19
19
  class Created < Praxis::Response
20
20
  self.status = 201
21
-
22
- def initialize(status:self.class.status, headers:{}, body:'', location: nil)
23
- super(status: status, headers: headers, body: body)
24
- headers['Location'] = location if location
25
- end
26
21
  end
27
22
 
28
23
 
@@ -138,94 +133,35 @@ module Praxis
138
133
 
139
134
  ApiDefinition.define do |api|
140
135
 
141
- api.response_template :accepted do
142
- status 202
143
- description "The request has been accepted for processing, but the processing has not been completed."
144
- end
145
-
146
- api.response_template :no_content do
147
- status 204
148
- description "The server successfully processed the request, but is not returning any content."
149
- end
150
-
151
- api.response_template :multiple_choices do
152
- status 300
153
- description "Indicates multiple options for the resource that the client may follow."
154
- end
155
-
156
- api.response_template :moved_permanently do
157
- status 301
158
- description "This and all future requests should be directed to the given URI."
159
- end
160
-
161
- api.response_template :found do
162
- status 302
163
- description "The requested resource resides temporarily under a different URI."
164
- end
165
-
166
- api.response_template :see_other do
167
- status 303
168
- description "The response to the request can be found under another URI using a GET method"
169
- end
170
-
171
- api.response_template :not_modified do
172
- status 304
173
- description "Indicates that the resource has not been modified since the version specified by the request headers If-Modified-Since or If-Match."
174
- end
175
-
176
- api.response_template :temporary_redirect do
177
- status 307
178
- description "In this case, the request should be repeated with another URI; however, future requests should still use the original URI."
179
- end
180
-
181
- api.response_template :bad_request do
182
- status 400
183
- description "The request cannot be fulfilled due to bad syntax."
184
- end
185
-
186
- api.response_template :unauthorized do
187
- status 401
188
- description "Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided."
189
- end
190
-
191
- api.response_template :forbidden do
192
- status 403
193
- description "The request was a valid request, but the server is refusing to respond to it."
194
- end
195
-
196
- api.response_template :not_found do
197
- status 404
198
- description "The requested resource could not be found but may be available again in the future."
199
- end
200
-
201
- api.response_template :method_not_allowed do
202
- status 405
203
- description "A request was made of a resource using a request method not supported by that resource."
204
- end
205
-
206
- api.response_template :not_acceptable do
207
- status 406
208
- description "The requested resource is only capable of generating content not acceptable according to the Accept headers sent in the request."
209
- end
210
-
211
- api.response_template :request_timeout do
212
- status 408
213
- description "The server timed out waiting for the request."
214
- end
215
-
216
- api.response_template :conflict do
217
- status 409
218
- description "Indicates that the request could not be processed because of conflict in the request, such as an edit conflict in the case of multiple updates."
219
- end
220
-
221
- api.response_template :precondition_failed do
222
- status 412
223
- description "The server does not meet one of the preconditions that the requester put on the request."
224
- end
225
136
 
226
- api.response_template :unprocessable_entity do
227
- status 422
228
- description "The request was well-formed but was unable to be followed due to semantic errors."
137
+ [
138
+ [ :accepted, 202, "The request has been accepted for processing, but the processing has not been completed." ],
139
+ [ :no_content, 204,"The server successfully processed the request, but is not returning any content."],
140
+ [ :multiple_choices, 300,"Indicates multiple options for the resource that the client may follow."],
141
+ [ :moved_permanently, 301,"This and all future requests should be directed to the given URI."],
142
+ [ :found, 302,"The requested resource resides temporarily under a different URI."],
143
+ [ :see_other, 303,"The response to the request can be found under another URI using a GET method"],
144
+ [ :not_modified, 304,"Indicates that the resource has not been modified since the version specified by the request headers If-Modified-Since or If-Match."],
145
+ [ :temporary_redirect, 307,"In this case, the request should be repeated with another URI; however, future requests should still use the original URI."],
146
+ [ :bad_request, 400,"The request cannot be fulfilled due to bad syntax."],
147
+ [ :unauthorized, 401,"Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided."],
148
+ [ :forbidden, 403,"The request was a valid request, but the server is refusing to respond to it."],
149
+ [ :not_found, 404,"The requested resource could not be found but may be available again in the future."],
150
+ [ :method_not_allowed, 405,"A request was made of a resource using a request method not supported by that resource."],
151
+ [ :not_acceptable, 406,"The requested resource is only capable of generating content not acceptable according to the Accept headers sent in the request."],
152
+ [ :request_timeout, 408,"The server timed out waiting for the request."],
153
+ [ :conflict, 409, "Indicates that the request could not be processed because of conflict in the request, such as an edit conflict in the case of multiple updates."],
154
+ [ :precondition_failed, 412,"The server does not meet one of the preconditions that the requester put on the request."],
155
+ [ :unprocessable_entity, 422,"The request was well-formed but was unable to be followed due to semantic errors."],
156
+ ].each do |name, code, base_description|
157
+ api.response_template name do |media_type: nil, location: nil, headers: nil, description: nil|
158
+ status code
159
+ description( description || base_description ) # description can "potentially" be overriden in an individual action.
160
+
161
+ media_type media_type if media_type
162
+ location location if location
163
+ headers headers if headers
164
+ end
229
165
  end
230
166
 
231
167
  end
@@ -3,7 +3,7 @@ module Praxis
3
3
  module Responses
4
4
 
5
5
  class ValidationError < BadRequest
6
- def initialize(summary:, errors: nil, exception: nil, **opts)
6
+ def initialize(summary:, errors: nil, exception: nil, documentation: nil, **opts)
7
7
  super(**opts)
8
8
  @headers['Content-Type'] = 'application/json' #TODO: might want an error mediatype
9
9
  @errors = errors
@@ -12,6 +12,7 @@ module Praxis
12
12
  end
13
13
  @exception = exception
14
14
  @summary = summary
15
+ @documentation = documentation
15
16
  end
16
17
 
17
18
  def format!
@@ -22,6 +23,8 @@ module Praxis
22
23
  @body[:cause] = {name: @exception.cause.class.name, message: @exception.cause.message}
23
24
  end
24
25
 
26
+ @body[:documentation] = @documentation if @documentation
27
+
25
28
  @body
26
29
  end
27
30
  end
@@ -1,6 +1,13 @@
1
1
  namespace :praxis do
2
2
 
3
3
  namespace :docs do
4
+
5
+ def base_path
6
+ require 'uri'
7
+ documentation_url = Praxis::ApiDefinition.instance.global_info.documentation_url
8
+ URI(documentation_url).path.gsub(/\/[^\/]*$/, '/') if documentation_url
9
+ end
10
+
4
11
  path = File.expand_path(File.join(File.dirname(__FILE__), '../../api_browser'))
5
12
 
6
13
  desc "Install dependencies"
@@ -32,7 +39,8 @@ namespace :praxis do
32
39
  exec({
33
40
  'USER_DOCS_PATH' => File.join(Dir.pwd, 'docs'),
34
41
  'DOC_PORT' => doc_port,
35
- 'PLUGIN_PATHS' => Praxis::Application.instance.doc_browser_plugin_paths.join(':')
42
+ 'PLUGIN_PATHS' => Praxis::Application.instance.doc_browser_plugin_paths.join(':'),
43
+ 'BASE_PATH' => '/'
36
44
  }, "#{path}/node_modules/.bin/grunt serve --gruntfile '#{path}/Gruntfile.js'")
37
45
  end
38
46
 
@@ -40,19 +48,11 @@ namespace :praxis do
40
48
  task :build => [:install, :generate] do
41
49
  exec({
42
50
  'USER_DOCS_PATH' => File.join(Dir.pwd, 'docs'),
43
- 'PLUGIN_PATHS' => Praxis::Application.instance.doc_browser_plugin_paths.join(':')
51
+ 'PLUGIN_PATHS' => Praxis::Application.instance.doc_browser_plugin_paths.join(':'),
52
+ 'BASE_PATH' => base_path
44
53
  }, "#{path}/node_modules/.bin/grunt build --gruntfile '#{path}/Gruntfile.js'")
45
54
  end
46
55
 
47
- desc "Generate deprecated API docs (JSON definitions) for a Praxis App"
48
- task :generate_old => [:environment] do |t, args|
49
- require 'fileutils'
50
- STDERR.puts "DEPRECATION: praxis:docs:generate_old is deprecated and will be removed in the next version. Please update tooling that may need this."
51
-
52
- Praxis::Blueprint.caching_enabled = false
53
- generator = Praxis::RestfulDocGenerator.new(Dir.pwd)
54
- end
55
-
56
56
  desc "Generate API docs (JSON definitions) for a Praxis App"
57
57
  task :generate => [:environment] do |t, args|
58
58
  require 'fileutils'
@@ -63,17 +63,4 @@ namespace :praxis do
63
63
  end
64
64
 
65
65
  end
66
-
67
- desc "Generate API docs (JSON definitions) for a Praxis App"
68
- task :api_docs do
69
- STDERR.puts "DEPRECATION: praxis:api_docs is deprecated and will be removed by 1.0. Please use praxis:docs:generate instead."
70
- Rake::Task["praxis:docs:generate_old"].invoke
71
- end
72
-
73
- desc "Run API Documentation Browser"
74
- task :doc_browser, [:port] do |t, args|
75
- STDERR.puts "DEPRECATION: praxis:doc_browser is deprecated and will be removed by 1.0. Please use praxis:docs:preview instead. The doc browser now runs on port 9090."
76
- Rake::Task["praxis:docs:preview"].invoke
77
- end
78
-
79
66
  end