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