praxis 0.21 → 0.22.pre.1

Sign up to get free protection for your applications and to get access to all the features.
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