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
@@ -12,6 +12,7 @@ module Praxis
12
12
 
13
13
  attr_reader :name
14
14
  attr_reader :resource_definition
15
+ attr_reader :api_definition
15
16
  attr_reader :routes
16
17
  attr_reader :primary_route
17
18
  attr_reader :named_routes
@@ -39,6 +40,7 @@ module Praxis
39
40
  @metadata = Hash.new
40
41
  @routes = []
41
42
  @traits = []
43
+ @api_definition = resource_definition.application.api_definition
42
44
 
43
45
  if (media_type = resource_definition.media_type)
44
46
  if media_type.kind_of?(Class) && media_type < Praxis::Types::MediaTypeCommon
@@ -47,7 +49,7 @@ module Praxis
47
49
  end
48
50
 
49
51
  version = resource_definition.version
50
- api_info = ApiDefinition.instance.info(resource_definition.version)
52
+ api_info = api_definition.info(resource_definition.version)
51
53
 
52
54
  route_base = "#{api_info.base_path}#{resource_definition.version_prefix}"
53
55
  prefix = Array(resource_definition.routing_prefix)
@@ -64,11 +66,11 @@ module Praxis
64
66
  end
65
67
 
66
68
  def trait(trait_name)
67
- unless ApiDefinition.instance.traits.has_key? trait_name
69
+ unless api_definition.traits.has_key? trait_name
68
70
  raise Exceptions::InvalidTrait.new("Trait #{trait_name} not found in the system")
69
71
  end
70
72
 
71
- trait = ApiDefinition.instance.traits.fetch(trait_name)
73
+ trait = api_definition.traits.fetch(trait_name)
72
74
  trait.apply!(self)
73
75
  traits << trait_name
74
76
  end
@@ -90,7 +92,7 @@ module Praxis
90
92
  args[:media_type] = type
91
93
  end
92
94
 
93
- template = ApiDefinition.instance.response(name)
95
+ template = api_definition.response(name)
94
96
  @responses[name] = template.compile(self, **args)
95
97
  end
96
98
 
@@ -303,7 +305,7 @@ module Praxis
303
305
 
304
306
  # and return that one if it already corresponds to a registered handler
305
307
  # otherwise, add the encoding
306
- if Praxis::Application.instance.handlers.include?(pick.handler_name)
308
+ if resource_definition.application.handlers.include?(pick.handler_name)
307
309
  return pick
308
310
  else
309
311
  return pick + handler_name
@@ -320,13 +322,13 @@ module Praxis
320
322
 
321
323
  hash[:examples] = {}
322
324
 
323
- default_handlers = ApiDefinition.instance.info.consumes
325
+ default_handlers = api_definition.info.consumes
324
326
 
325
327
  default_handlers.each do |default_handler|
326
328
  dumped_payload = payload.dump(example, default_format: default_handler)
327
329
 
328
330
  content_type = derive_content_type(example, default_handler)
329
- handler = Praxis::Application.instance.handlers[content_type.handler_name]
331
+ handler = resource_definition.application.handlers[content_type.handler_name]
330
332
 
331
333
  # in case handler is nil, use dumped_payload as-is.
332
334
  generated_payload = if handler.nil?
@@ -1,41 +1,57 @@
1
- require 'singleton'
2
1
  require 'forwardable'
3
2
 
4
3
  module Praxis
5
4
 
6
5
  class ApiDefinition
7
- include Singleton
8
6
  extend Forwardable
9
7
 
10
8
  attr_reader :traits
11
9
  attr_reader :responses
12
10
  attr_reader :infos
13
11
  attr_reader :global_info
12
+ attr_reader :application
14
13
 
15
14
  attr_accessor :versioning_scheme
16
15
 
16
+ def self.instance
17
+ i = Thread.current[:praxis_instance] || $praxis_initializing_instance
18
+ raise "Trying to use Praxis::ApiDefinition outside the context of a Praxis::Application" unless i
19
+ i.api_definition
20
+ end
21
+
17
22
  def self.define(&block)
23
+
24
+ definition = Praxis::Application.current_instance.api_definition
25
+ if block.arity == 0
26
+ definition.instance_eval(&block)
27
+ else
28
+ yield(definition)
29
+ end
30
+ end
31
+
32
+ def define(&block)
18
33
  if block.arity == 0
19
- self.instance.instance_eval(&block)
34
+ self.instance_eval(&block)
20
35
  else
21
- yield(self.instance)
36
+ yield(self)
22
37
  end
23
38
  end
24
39
 
25
- def initialize
40
+ def initialize(application)
41
+ @application = application
26
42
  @responses = Hash.new
27
43
  @traits = Hash.new
28
44
  @base_path = ''
29
45
 
30
- @global_info = ApiGeneralInfo.new
46
+ @global_info = ApiGeneralInfo.new(application: application)
31
47
 
32
48
  @infos = Hash.new do |hash, version|
33
- hash[version] = ApiGeneralInfo.new(@global_info, version: version)
49
+ hash[version] = ApiGeneralInfo.new(@global_info, application: application, version: version)
34
50
  end
35
51
  end
36
52
 
37
53
  def response_template(name, &block)
38
- @responses[name] = Praxis::ResponseTemplate.new(name, &block)
54
+ @responses[name] = Praxis::ResponseTemplate.new(name, application, &block)
39
55
  end
40
56
 
41
57
  def response(name)
@@ -91,25 +107,26 @@ module Praxis
91
107
  data
92
108
  end
93
109
 
94
- define do |api|
95
- api.response_template :ok do |media_type: , location: nil, headers: nil, description: nil |
96
- status 200
97
- description( description || 'Standard response for successful HTTP requests.' )
98
-
99
- media_type media_type
100
- location location
101
- headers headers if headers
102
- end
103
-
104
- api.response_template :created do |media_type: nil, location: nil, headers: nil, description: nil|
105
- status 201
106
- description( description || 'The request has been fulfilled and resulted in a new resource being created.' )
107
-
108
- media_type media_type if media_type
109
- location location
110
- headers headers if headers
111
- end
112
- end
110
+ # CANNOT DEFINE IT AT FILE LOADING TIME: THE INSTANCE FOR THE API_DEFINITION IS NOT READY YET.
111
+ # define do |api|
112
+ # api.response_template :ok do |media_type: , location: nil, headers: nil, description: nil |
113
+ # status 200
114
+ # description( description || 'Standard response for successful HTTP requests.' )
115
+ #
116
+ # media_type media_type
117
+ # location location
118
+ # headers headers if headers
119
+ # end
120
+ #
121
+ # api.response_template :created do |media_type: nil, location: nil, headers: nil, description: nil|
122
+ # status 201
123
+ # description( description || 'The request has been fulfilled and resulted in a new resource being created.' )
124
+ #
125
+ # media_type media_type if media_type
126
+ # location location
127
+ # headers headers if headers
128
+ # end
129
+ # end
113
130
 
114
131
  end
115
132
 
@@ -3,10 +3,11 @@ module Praxis
3
3
 
4
4
  attr_reader :version
5
5
 
6
- def initialize(global_info=nil, version: nil)
6
+ def initialize(global_info=nil, application:, version: nil)
7
7
  @data = Hash.new
8
8
  @global_info = global_info
9
9
  @version = version
10
+ @application = application
10
11
 
11
12
  if @global_info.nil? # this *is* the global info
12
13
  version_with [:header, :params]
@@ -54,7 +55,7 @@ module Praxis
54
55
  get(:version_with)
55
56
  else
56
57
  if @global_info.nil? # this *is* the global info
57
- Application.instance.versioning_scheme = val
58
+ @application.versioning_scheme = val
58
59
  set(:version_with, val)
59
60
  else
60
61
  raise "Use of version_with is only allowed in the global part of " \
@@ -1,16 +1,15 @@
1
- require 'singleton'
2
1
  require 'mustermann'
3
2
  require 'logger'
4
3
 
5
4
  module Praxis
6
5
  class Application
7
- include Singleton
8
6
 
9
7
  attr_reader :router
10
8
  attr_reader :controllers
11
9
  attr_reader :resource_definitions
12
10
  attr_reader :app
13
11
  attr_reader :builder
12
+ attr_reader :api_definition
14
13
 
15
14
  attr_accessor :bootloader
16
15
  attr_accessor :file_layout
@@ -25,12 +24,30 @@ module Praxis
25
24
 
26
25
  attr_accessor :versioning_scheme
27
26
 
27
+ @@registered_apps = {}
28
28
 
29
+ def self.registered_apps
30
+ @@registered_apps
31
+ end
32
+
33
+ def self.instance
34
+ i = current_instance
35
+ return i if i
36
+ $praxis_initializing_instance = self.new
37
+ end
38
+
39
+ def self.current_instance
40
+ Thread.current[:praxis_instance] || $praxis_initializing_instance
41
+ end
42
+
29
43
  def self.configure
30
- yield(self.instance)
44
+ # Should fail (i.e., be nil) if it's not in initialization/setup or a runtime call
45
+ yield(current_instance)
31
46
  end
32
47
 
33
- def initialize
48
+ def initialize(name: 'default', skip_registration: false)
49
+ old = $praxis_initializing_instance
50
+ $praxis_initializing_instance = self # ApiDefinition.new needs to get the instance...
34
51
  @controllers = Set.new
35
52
  @resource_definitions = Set.new
36
53
 
@@ -51,37 +68,135 @@ module Praxis
51
68
  @config = Config.new
52
69
  @root = nil
53
70
  @logger = Logger.new(STDOUT)
54
- end
55
-
56
-
57
- def setup(root: '.')
58
- return self unless @app.nil?
59
-
60
- @root = Pathname.new(root).expand_path
71
+ @api_definition = ApiDefinition.new(self)
72
+
73
+ @api_definition.define do |api|
74
+ api.response_template :ok do |media_type: , location: nil, headers: nil, description: nil |
75
+ status 200
76
+ description( description || 'Standard response for successful HTTP requests.' )
77
+
78
+ media_type media_type
79
+ location location
80
+ headers headers if headers
81
+ end
82
+
83
+ api.response_template :created do |media_type: nil, location: nil, headers: nil, description: nil|
84
+ status 201
85
+ description( description || 'The request has been fulfilled and resulted in a new resource being created.' )
86
+
87
+ media_type media_type if media_type
88
+ location location
89
+ headers headers if headers
90
+ end
91
+ end
92
+
93
+ require 'praxis/responses/http'
94
+ self.api_definition.define do |api|
95
+ [
96
+ [ :accepted, 202, "The request has been accepted for processing, but the processing has not been completed." ],
97
+ [ :no_content, 204,"The server successfully processed the request, but is not returning any content."],
98
+ [ :multiple_choices, 300,"Indicates multiple options for the resource that the client may follow."],
99
+ [ :moved_permanently, 301,"This and all future requests should be directed to the given URI."],
100
+ [ :found, 302,"The requested resource resides temporarily under a different URI."],
101
+ [ :see_other, 303,"The response to the request can be found under another URI using a GET method"],
102
+ [ :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."],
103
+ [ :temporary_redirect, 307,"In this case, the request should be repeated with another URI; however, future requests should still use the original URI."],
104
+ [ :bad_request, 400,"The request cannot be fulfilled due to bad syntax."],
105
+ [ :unauthorized, 401,"Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided."],
106
+ [ :forbidden, 403,"The request was a valid request, but the server is refusing to respond to it."],
107
+ [ :not_found, 404,"The requested resource could not be found but may be available again in the future."],
108
+ [ :method_not_allowed, 405,"A request was made of a resource using a request method not supported by that resource."],
109
+ [ :not_acceptable, 406,"The requested resource is only capable of generating content not acceptable according to the Accept headers sent in the request."],
110
+ [ :request_timeout, 408,"The server timed out waiting for the request."],
111
+ [ :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."],
112
+ [ :precondition_failed, 412,"The server does not meet one of the preconditions that the requester put on the request."],
113
+ [ :unprocessable_entity, 422,"The request was well-formed but was unable to be followed due to semantic errors."],
114
+ ].each do |name, code, base_description|
115
+ api.response_template name do |media_type: nil, location: nil, headers: nil, description: nil|
116
+ status code
117
+ description( description || base_description ) # description can "potentially" be overriden in an individual action.
118
+
119
+ media_type media_type if media_type
120
+ location location if location
121
+ headers headers if headers
122
+ end
123
+ end
61
124
 
125
+ end
126
+
127
+ require 'praxis/responses/internal_server_error'
128
+ self.api_definition.define do |api|
129
+ api.response_template :internal_server_error do
130
+ description "A generic error message, given when an unexpected condition was encountered and no more specific message is suitable."
131
+ status 500
132
+ media_type "application/json"
133
+ end
134
+ end
135
+
136
+ require 'praxis/responses/validation_error'
137
+ self.api_definition.define do |api|
138
+ api.response_template :validation_error do
139
+ description "An error message indicating that one or more elements of the request did not match the API specification for the action"
140
+ status 400
141
+ media_type "application/json"
142
+ end
143
+ end
144
+
145
+
146
+ require 'praxis/responses/multipart_ok'
147
+ self.api_definition.define do |api|
148
+ api.response_template :multipart_ok do |media_type: Praxis::Types::MultipartArray|
149
+ status 200
150
+ media_type media_type
151
+ end
152
+ end
153
+
62
154
  builtin_handlers = {
63
155
  'plain' => Praxis::Handlers::Plain,
64
156
  'json' => Praxis::Handlers::JSON,
65
157
  'x-www-form-urlencoded' => Praxis::Handlers::WWWForm
66
158
  }
159
+
67
160
  # Register built-in handlers unless the app already provided its own
68
161
  builtin_handlers.each_pair do |name, handler|
69
162
  self.handler(name, handler) unless handlers.key?(name)
70
163
  end
164
+
165
+ setup_initial_config!
166
+
167
+ unless skip_registration
168
+ if self.class.registered_apps[name]
169
+ raise "A Praxis instance named #{name} has already been registered, please use the :name parameter to initialize them"
170
+ end
171
+ self.class.registered_apps[name] = self
172
+ end
173
+ $praxis_initializing_instance = old
174
+ end
175
+
176
+ def setup_initial_config!
177
+ self.config do
178
+ attribute :praxis do
179
+ attribute :validate_responses, Attributor::Boolean, default: false
180
+ attribute :validate_response_bodies, Attributor::Boolean, default: false
71
181
 
72
- @bootloader.setup!
182
+ attribute :show_exceptions, Attributor::Boolean, default: false
183
+ attribute :x_cascade, Attributor::Boolean, default: true
184
+ end
185
+ end
186
+ end
73
187
 
74
- @builder.run(@router)
75
- @app = @builder.to_app
76
188
 
77
- Notifications.subscribe 'rack.request.all'.freeze do |name, start, finish, _id, payload|
78
- duration = (finish - start) * 1000
79
- Stats.timing(name, duration)
189
+ def setup(root: '.')
190
+ return self unless @app.nil?
191
+ saved_value = $praxis_initializing_instance
192
+ $praxis_initializing_instance = self
193
+ @root = Pathname.new(root).expand_path
80
194
 
81
- status, _, _ = payload[:response]
82
- Stats.increment "rack.request.#{status}"
83
- end
195
+ bootloader.setup!
196
+ builder.run(@router)
197
+ @app = builder.to_app
84
198
 
199
+ $praxis_initializing_instance = saved_value
85
200
  self
86
201
  end
87
202
 
@@ -111,9 +226,13 @@ module Praxis
111
226
 
112
227
  def call(env)
113
228
  response = []
229
+ old = Thread.current[:praxis_instance]
230
+ Thread.current[:praxis_instance] = self
114
231
  Notifications.instrument 'rack.request.all'.freeze, response: response do
115
232
  response.push(*@app.call(env))
116
233
  end
234
+ ensure
235
+ Thread.current[:praxis_instance] = old
117
236
  end
118
237
 
119
238
  def layout(&block)
@@ -49,7 +49,7 @@ module Praxis
49
49
  after(:app) do
50
50
  Praxis::Mapper.finalize!
51
51
  Praxis::Blueprint.finalize!
52
- Praxis::ResourceDefinition.finalize!
52
+ Praxis::ResourceDefinition.finalize!(application: self.application)
53
53
  end
54
54
 
55
55
  end
@@ -112,10 +112,8 @@ module Praxis
112
112
  end
113
113
 
114
114
  def setup!
115
- # use the Stats and Notifications plugins by default
116
- use Praxis::Stats
115
+ # use the Notifications plugin by default
117
116
  use Praxis::Notifications
118
-
119
117
  run
120
118
  end
121
119
 
@@ -8,7 +8,6 @@ module Praxis
8
8
  # 1) the environment.rb file - generic stuff for all environments
9
9
  # 2) "Deployer.environment".rb - environment specific stuff
10
10
  def execute
11
- setup_initial_config!
12
11
 
13
12
  env_file = application.root + "config/environment.rb"
14
13
  require env_file if File.exists? env_file
@@ -37,18 +36,6 @@ module Praxis
37
36
  end
38
37
  end
39
38
 
40
- # TODO: not really sure I like this here... but where else is better?
41
- def setup_initial_config!
42
- application.config do
43
- attribute :praxis do
44
- attribute :validate_responses, Attributor::Boolean, default: false
45
- attribute :validate_response_bodies, Attributor::Boolean, default: false
46
-
47
- attribute :show_exceptions, Attributor::Boolean, default: false
48
- attribute :x_cascade, Attributor::Boolean, default: true
49
- end
50
- end
51
- end
52
39
 
53
40
  end
54
41