praxis 0.20.1 → 0.21

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