praxis 0.21 → 0.22.pre.1

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 (91) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +20 -12
  3. data/CHANGELOG.md +24 -0
  4. data/CONTRIBUTING.md +4 -4
  5. data/README.md +11 -9
  6. data/lib/api_browser/app/js/directives/attribute_table.js +2 -1
  7. data/lib/api_browser/app/js/directives/conditional_requirements.js +13 -0
  8. data/lib/api_browser/app/js/directives/type_placeholder.js +10 -1
  9. data/lib/api_browser/app/js/factories/normalize_attributes.js +4 -2
  10. data/lib/api_browser/app/js/factories/template_for.js +5 -2
  11. data/lib/api_browser/app/js/filters/has_requirement.js +14 -0
  12. data/lib/api_browser/app/js/filters/tag_requirement.js +13 -0
  13. data/lib/api_browser/app/sass/praxis.scss +11 -0
  14. data/lib/api_browser/app/views/action.html +2 -2
  15. data/lib/api_browser/app/views/directives/attribute_description/member_options.html +2 -2
  16. data/lib/api_browser/app/views/directives/attribute_table.html +1 -1
  17. data/lib/api_browser/app/views/type.html +1 -1
  18. data/lib/api_browser/app/views/type/details.html +2 -2
  19. data/lib/api_browser/app/views/types/embedded/array.html +2 -0
  20. data/lib/api_browser/app/views/types/embedded/default.html +3 -1
  21. data/lib/api_browser/app/views/types/embedded/requirements.html +6 -0
  22. data/lib/api_browser/app/views/types/embedded/single_req.html +9 -0
  23. data/lib/api_browser/app/views/types/embedded/struct.html +14 -2
  24. data/lib/api_browser/app/views/types/standalone/array.html +1 -1
  25. data/lib/api_browser/app/views/types/standalone/struct.html +2 -1
  26. data/lib/api_browser/package.json +1 -1
  27. data/lib/praxis.rb +8 -6
  28. data/lib/praxis/action_definition.rb +9 -7
  29. data/lib/praxis/api_definition.rb +44 -27
  30. data/lib/praxis/api_general_info.rb +3 -2
  31. data/lib/praxis/application.rb +139 -20
  32. data/lib/praxis/bootloader.rb +2 -4
  33. data/lib/praxis/bootloader_stages/environment.rb +0 -13
  34. data/lib/praxis/controller.rb +2 -0
  35. data/lib/praxis/dispatcher.rb +16 -10
  36. data/lib/praxis/docs/generator.rb +20 -9
  37. data/lib/praxis/docs/link_builder.rb +1 -1
  38. data/lib/praxis/error_handler.rb +5 -5
  39. data/lib/praxis/extensions/attribute_filtering.rb +28 -0
  40. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +180 -0
  41. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +273 -0
  42. data/lib/praxis/extensions/attribute_filtering/query_builder.rb +39 -0
  43. data/lib/praxis/extensions/field_selection.rb +3 -0
  44. data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +57 -0
  45. data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +65 -0
  46. data/lib/praxis/extensions/rails_compat.rb +2 -0
  47. data/lib/praxis/extensions/rails_compat/request_methods.rb +19 -0
  48. data/lib/praxis/extensions/rendering.rb +1 -1
  49. data/lib/praxis/file_group.rb +1 -1
  50. data/lib/praxis/middleware_app.rb +26 -6
  51. data/lib/praxis/multipart/parser.rb +14 -2
  52. data/lib/praxis/multipart/part.rb +5 -3
  53. data/lib/praxis/plugins/praxis_mapper_plugin.rb +2 -2
  54. data/lib/praxis/plugins/rails_plugin.rb +104 -0
  55. data/lib/praxis/request.rb +8 -9
  56. data/lib/praxis/request_stages/response.rb +3 -2
  57. data/lib/praxis/request_superclassing.rb +11 -0
  58. data/lib/praxis/resource_definition.rb +14 -10
  59. data/lib/praxis/response.rb +6 -7
  60. data/lib/praxis/response_definition.rb +7 -5
  61. data/lib/praxis/response_template.rb +4 -3
  62. data/lib/praxis/responses/http.rb +0 -36
  63. data/lib/praxis/responses/internal_server_error.rb +3 -12
  64. data/lib/praxis/responses/multipart_ok.rb +4 -11
  65. data/lib/praxis/responses/validation_error.rb +1 -10
  66. data/lib/praxis/router.rb +3 -3
  67. data/lib/praxis/tasks/api_docs.rb +10 -2
  68. data/lib/praxis/tasks/routes.rb +1 -0
  69. data/lib/praxis/version.rb +1 -1
  70. data/praxis.gemspec +4 -5
  71. data/spec/functional_spec.rb +4 -6
  72. data/spec/praxis/action_definition_spec.rb +26 -15
  73. data/spec/praxis/api_definition_spec.rb +13 -8
  74. data/spec/praxis/api_general_info_spec.rb +3 -8
  75. data/spec/praxis/application_spec.rb +13 -7
  76. data/spec/praxis/middleware_app_spec.rb +24 -10
  77. data/spec/praxis/request_spec.rb +17 -7
  78. data/spec/praxis/request_stages/validate_spec.rb +1 -1
  79. data/spec/praxis/resource_definition_spec.rb +12 -10
  80. data/spec/praxis/response_definition_spec.rb +22 -5
  81. data/spec/praxis/response_spec.rb +12 -5
  82. data/spec/praxis/responses/internal_server_error_spec.rb +4 -7
  83. data/spec/praxis/responses/validation_error_spec.rb +2 -2
  84. data/spec/praxis/router_spec.rb +8 -4
  85. data/spec/spec_app/config.ru +1 -6
  86. data/spec/spec_helper.rb +3 -3
  87. data/tasks/thor/templates/generator/empty_app/Gemfile +3 -3
  88. metadata +36 -32
  89. data/.ruby-version +0 -1
  90. data/lib/praxis/stats.rb +0 -113
  91. data/spec/praxis/stats_spec.rb +0 -9
@@ -1,7 +1,7 @@
1
1
  module Praxis
2
2
 
3
- class Request
4
- attr_reader :env, :query
3
+ class Request < Praxis.request_superclass
4
+ attr_reader :env, :query, :praxis_instance
5
5
  attr_accessor :route_params, :action
6
6
 
7
7
  PATH_VERSION_PREFIX = "/v".freeze
@@ -14,10 +14,11 @@ module Praxis
14
14
  API_NO_VERSION_NAME = 'n/a'.freeze
15
15
  VERSION_USING_DEFAULTS = [:header, :params].freeze
16
16
 
17
- def initialize(env)
17
+ def initialize(env, args={})
18
18
  @env = env
19
19
  @query = Rack::Utils.parse_nested_query(env[QUERY_STRING_NAME])
20
20
  @route_params = {}
21
+ @praxis_instance = args[:application]
21
22
  end
22
23
 
23
24
  # Determine the content type of this request as indicated by the Content-Type header.
@@ -86,8 +87,8 @@ module Praxis
86
87
  PATH_VERSION_MATCHER = %r{^#{self.path_version_prefix}(?<version>[^\/]+)\/}.freeze
87
88
 
88
89
  def path_version_matcher
89
- if Application.instance.versioning_scheme == :path
90
- matcher = Mustermann.new(ApiDefinition.instance.info.base_path + '*')
90
+ if praxis_instance.versioning_scheme == :path
91
+ matcher = Mustermann.new(praxis_instance.api_definition.info.base_path + '*')
91
92
  matcher.params(self.path)[API_VERSION_PARAM_NAME]
92
93
  else
93
94
  PATH_VERSION_MATCHER.match(self.path)[:version]
@@ -96,8 +97,7 @@ module Praxis
96
97
 
97
98
  def version
98
99
  result = nil
99
-
100
- Array(Application.instance.versioning_scheme).find do |mode|
100
+ Array(praxis_instance.versioning_scheme).find do |mode|
101
101
  case mode
102
102
  when :header;
103
103
  result = env[API_VERSION_HEADER_NAME]
@@ -128,8 +128,7 @@ module Praxis
128
128
  def load_payload(context)
129
129
  return unless action.payload
130
130
  return if content_type.nil?
131
-
132
- raw = if (handler = Praxis::Application.instance.handlers[content_type.handler_name])
131
+ raw = if (handler = praxis_instance.handlers[content_type.handler_name])
133
132
  handler.parse(self.raw_payload)
134
133
  else
135
134
  # TODO is this a good default?
@@ -8,8 +8,9 @@ module Praxis
8
8
 
9
9
  response.handle
10
10
 
11
- if Application.instance.config.praxis.validate_responses == true
12
- validate_body = Application.instance.config.praxis.validate_response_bodies
11
+ config = Application.current_instance.config
12
+ if config.praxis.validate_responses == true
13
+ validate_body = config.praxis.validate_response_bodies
13
14
 
14
15
  response.validate(action, validate_body: validate_body)
15
16
  end
@@ -0,0 +1,11 @@
1
+ module Praxis
2
+ class << self
3
+
4
+ attr_writer :request_superclass
5
+
6
+ def request_superclass
7
+ @request_superclass || Object
8
+ end
9
+
10
+ end
11
+ end
@@ -8,11 +8,15 @@ module Praxis
8
8
  DEFAULT_RESOURCE_HREF_ACTION = :show
9
9
 
10
10
  included do
11
+ # Store the attached (i.e., current) Praxis App instance into the resource definition for easy retrieval later
12
+ @application = Application.instance
13
+ @application.resource_definitions << self
14
+
11
15
  @version = 'n/a'.freeze
12
16
  @actions = Hash.new
13
17
  @responses = Hash.new
14
18
 
15
- @action_defaults = Trait.new &ResourceDefinition.generate_defaults_block
19
+ @action_defaults = Trait.new &ResourceDefinition.generate_defaults_block(application: @application)
16
20
 
17
21
  @version_options = {}
18
22
  @metadata = {}
@@ -33,13 +37,12 @@ module Praxis
33
37
 
34
38
  @on_finalize = Array.new
35
39
 
36
- Application.instance.resource_definitions << self
40
+
37
41
  end
38
42
 
39
- def self.generate_defaults_block( version: nil )
40
-
43
+ def self.generate_defaults_block( version: nil, application:)
41
44
  # 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)
45
+ base_attributes = if (base_params = application.api_definition.info(version).base_params)
43
46
  base_params.attributes
44
47
  else
45
48
  {}
@@ -56,8 +59,8 @@ module Praxis
56
59
  end
57
60
  end
58
61
 
59
- def self.finalize!
60
- Application.instance.resource_definitions.each do |resource_definition|
62
+ def self.finalize!(application: )
63
+ application.resource_definitions.each do |resource_definition|
61
64
  while (block = resource_definition.on_finalize.shift)
62
65
  block.call
63
66
  end
@@ -73,6 +76,7 @@ module Praxis
73
76
  attr_reader :traits
74
77
  attr_reader :version_prefix
75
78
  attr_reader :parent_prefix
79
+ attr_reader :application
76
80
 
77
81
  # opaque hash of user-defined medata, used to decorate the definition,
78
82
  # and also available in the generated JSON documents
@@ -199,7 +203,7 @@ module Praxis
199
203
  end
200
204
  end
201
205
 
202
- @action_defaults.instance_eval &ResourceDefinition.generate_defaults_block( version: version )
206
+ @action_defaults.instance_eval &ResourceDefinition.generate_defaults_block( version: version, application: self.application)
203
207
  end
204
208
 
205
209
 
@@ -238,10 +242,10 @@ module Praxis
238
242
  end
239
243
 
240
244
  def trait(trait_name)
241
- unless ApiDefinition.instance.traits.has_key? trait_name
245
+ unless self.application.api_definition.traits.has_key? trait_name
242
246
  raise Exceptions::InvalidTrait.new("Trait #{trait_name} not found in the system")
243
247
  end
244
- trait = ApiDefinition.instance.traits.fetch(trait_name)
248
+ trait = self.application.api_definition.traits.fetch(trait_name)
245
249
  @traits << trait_name
246
250
  end
247
251
  alias_method :use, :trait
@@ -18,7 +18,7 @@ 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:'', location: nil)
21
+ def initialize(status:self.class.status, headers:{}, body: nil, location: nil)
22
22
  @name = response_name
23
23
  @status = status
24
24
  @headers = headers
@@ -63,24 +63,23 @@ module Praxis
63
63
  self.class.response_name
64
64
  end
65
65
 
66
- def format!
66
+ def format!(config:)
67
67
  end
68
68
 
69
- def encode!
69
+ def encode!(handlers:)
70
70
  case @body
71
71
  when Hash, Array
72
72
  # response payload is structured data; transform it into an entity using the handler
73
73
  # implied by the response's media type. If no handler is registered for this
74
74
  # name, assume JSON as a default handler.
75
- handlers = Praxis::Application.instance.handlers
76
75
  handler = (content_type && handlers[content_type.handler_name]) || handlers['json']
77
76
  @body = handler.generate(@body)
78
77
  end
79
78
  end
80
79
 
81
- def finish
82
- format!
83
- encode!
80
+ def finish(application:)
81
+ format!(config: application.config)
82
+ encode!(handlers: application.handlers)
84
83
 
85
84
  @body = Array(@body)
86
85
 
@@ -4,8 +4,9 @@ module Praxis
4
4
 
5
5
  class ResponseDefinition
6
6
  attr_reader :name
7
-
8
- def initialize(response_name, **spec, &block)
7
+ attr_reader :application
8
+
9
+ def initialize(response_name, application, **spec, &block)
9
10
  unless response_name
10
11
  raise Exceptions::InvalidConfiguration.new(
11
12
  "Response name is required for a response specification"
@@ -13,6 +14,7 @@ module Praxis
13
14
  end
14
15
  @spec = { headers:{} }
15
16
  @name = response_name
17
+ @application = application
16
18
  self.instance_exec(**spec, &block) if block_given?
17
19
 
18
20
  if self.status.nil?
@@ -141,9 +143,9 @@ module Praxis
141
143
  # FIXME: remove load when when MediaTypeCommon.identifier returns a MediaTypeIdentifier
142
144
  identifier = MediaTypeIdentifier.load(self.media_type.identifier)
143
145
 
144
- default_handlers = ApiDefinition.instance.info.produces
146
+ default_handlers = application.api_definition.info.produces
145
147
 
146
- handlers = Praxis::Application.instance.handlers.select do |k,v|
148
+ handlers = application.handlers.select do |k,v|
147
149
  default_handlers.include?(k)
148
150
  end
149
151
 
@@ -200,7 +202,7 @@ module Praxis
200
202
  raise ArgumentError, "Parts definition for response #{name} does not allow :like and a block simultaneously"
201
203
  end
202
204
  if like
203
- template = ApiDefinition.instance.response(like)
205
+ template = application.api_definition.response(like)
204
206
  @parts = template.compile(nil, **args)
205
207
  else # block
206
208
  @parts = Praxis::ResponseDefinition.new('anonymous', **args, &a_proc)
@@ -1,11 +1,12 @@
1
1
  module Praxis
2
2
 
3
3
  class ResponseTemplate
4
- attr_reader :name, :block
4
+ attr_reader :name, :block, :application
5
5
 
6
- def initialize(response_name, &block)
6
+ def initialize(response_name, application, &block)
7
7
  @name = response_name
8
8
  @block = block
9
+ @application = application
9
10
  end
10
11
 
11
12
  def compile(action=nil, **args)
@@ -23,7 +24,7 @@ module Praxis
23
24
  args[:media_type] = media_type
24
25
  end
25
26
  end
26
- Praxis::ResponseDefinition.new(name, **args, &block)
27
+ Praxis::ResponseDefinition.new(name, application, **args, &block)
27
28
  end
28
29
 
29
30
  def describe
@@ -131,41 +131,5 @@ module Praxis
131
131
  self.status = 422
132
132
  end
133
133
 
134
- ApiDefinition.define do |api|
135
-
136
-
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
165
- end
166
-
167
- end
168
-
169
-
170
134
  end
171
135
  end
@@ -13,16 +13,15 @@ module Praxis
13
13
  @error = error
14
14
  end
15
15
 
16
- def format!(exception = @error)
16
+ def format!(exception = @error, config:)
17
17
  if @error
18
-
19
- if Application.instance.config.praxis.show_exceptions == true
18
+ if config.praxis.show_exceptions == true
20
19
  msg = {
21
20
  name: exception.class.name,
22
21
  message: exception.message,
23
22
  backtrace: exception.backtrace
24
23
  }
25
- msg[:cause] = format!(exception.cause) if exception.cause
24
+ msg[:cause] = format!(exception.cause, config: config) if exception.cause
26
25
  else
27
26
  msg = {name: 'InternalServerError', message: "Something bad happened."}
28
27
  end
@@ -34,12 +33,4 @@ module Praxis
34
33
 
35
34
  end
36
35
 
37
- ApiDefinition.define do |api|
38
- api.response_template :internal_server_error do
39
- description "A generic error message, given when an unexpected condition was encountered and no more specific message is suitable."
40
- status 500
41
- media_type "application/json"
42
- end
43
- end
44
-
45
36
  end
@@ -19,7 +19,7 @@ module Praxis
19
19
  end
20
20
 
21
21
 
22
- def encode!
22
+ def encode!(handlers:)
23
23
  case @body
24
24
  when Praxis::Types::MultipartArray
25
25
  @body = @body.dump
@@ -28,9 +28,9 @@ module Praxis
28
28
  end
29
29
  end
30
30
 
31
- def finish
32
- format!
33
- encode!
31
+ def finish(application:)
32
+ format!(config: application.config)
33
+ encode!(handlers: application.handlers)
34
34
 
35
35
  @body = Array(@body)
36
36
 
@@ -41,11 +41,4 @@ module Praxis
41
41
 
42
42
  end
43
43
 
44
- ApiDefinition.define do |api|
45
- api.response_template :multipart_ok do |media_type: Praxis::Types::MultipartArray|
46
- status 200
47
- media_type media_type
48
- end
49
- end
50
-
51
44
  end
@@ -15,7 +15,7 @@ module Praxis
15
15
  @documentation = documentation
16
16
  end
17
17
 
18
- def format!
18
+ def format!(**_args)
19
19
  @body = {name: 'ValidationError', summary: @summary }
20
20
  @body[:errors] = @errors if @errors
21
21
 
@@ -31,13 +31,4 @@ module Praxis
31
31
 
32
32
  end
33
33
 
34
-
35
- ApiDefinition.define do |api|
36
- api.response_template :validation_error do
37
- description "An error message indicating that one or more elements of the request did not match the API specification for the action"
38
- status 400
39
- media_type "application/json"
40
- end
41
- end
42
-
43
34
  end
data/lib/praxis/router.rb CHANGED
@@ -50,7 +50,7 @@ module Praxis
50
50
  end
51
51
 
52
52
  def add_route(target, route)
53
- path_versioning = (Application.instance.versioning_scheme == :path)
53
+ path_versioning = (application.versioning_scheme == :path)
54
54
 
55
55
  # DEPRECATED: remove with ResourceDefinition.version using: :path
56
56
  path_versioning ||= (target.action.resource_definition.version_options[:using] == :path)
@@ -65,7 +65,7 @@ module Praxis
65
65
  def call(env_or_request)
66
66
  request = case env_or_request
67
67
  when Hash
68
- request_class.new(env_or_request)
68
+ request_class.new(env_or_request, application: application)
69
69
  when request_class
70
70
  env_or_request
71
71
  else
@@ -101,7 +101,7 @@ module Praxis
101
101
  body += " Available versions = #{pretty_versions}."
102
102
  end
103
103
  headers = {"Content-Type" => "text/plain"}
104
- if Praxis::Application.instance.config.praxis.x_cascade
104
+ if application.config.praxis.x_cascade
105
105
  headers['X-Cascade'] = 'pass'
106
106
  end
107
107
  result = [404, headers, [body]]
@@ -58,8 +58,16 @@ namespace :praxis do
58
58
  require 'fileutils'
59
59
 
60
60
  Praxis::Blueprint.caching_enabled = false
61
- generator = Praxis::Docs::Generator.new(Dir.pwd)
62
- generator.save!
61
+
62
+ apps = Praxis::Application.registered_apps
63
+ if apps.size == 1
64
+ # Backwards compatible directory generation when there's only 1 app
65
+ Praxis::Docs::Generator.generate(Dir.pwd, name: apps.first[0], skip_sub_directory: true)
66
+ else
67
+ apps.each do|name,instance|
68
+ Praxis::Docs::Generator.generate(Dir.pwd, name: name)
69
+ end
70
+ end
63
71
  end
64
72
 
65
73
  end
@@ -11,6 +11,7 @@ namespace :praxis do
11
11
  ]
12
12
 
13
13
  rows = []
14
+ # TODO SINGLETON: ... what do do here?...
14
15
  Praxis::Application.instance.resource_definitions.each do |resource_definition|
15
16
  resource_definition.actions.each do |name, action|
16
17
  method = begin
@@ -1,3 +1,3 @@
1
1
  module Praxis
2
- VERSION = '0.21'
2
+ VERSION = '0.22.pre.1'
3
3
  end