praxis 0.22.pre.2 → 2.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +323 -324
- data/lib/praxis/action_definition.rb +7 -9
- data/lib/praxis/api_definition.rb +27 -44
- data/lib/praxis/api_general_info.rb +2 -3
- data/lib/praxis/application.rb +14 -141
- data/lib/praxis/bootloader.rb +1 -2
- data/lib/praxis/bootloader_stages/environment.rb +13 -0
- data/lib/praxis/controller.rb +0 -2
- data/lib/praxis/dispatcher.rb +4 -6
- data/lib/praxis/docs/generator.rb +8 -18
- data/lib/praxis/docs/link_builder.rb +1 -1
- data/lib/praxis/error_handler.rb +5 -5
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +1 -1
- data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +125 -0
- data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +16 -18
- data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +5 -5
- data/lib/praxis/extensions/field_selection.rb +1 -12
- data/lib/praxis/extensions/rendering.rb +1 -1
- data/lib/praxis/file_group.rb +1 -1
- data/lib/praxis/handlers/xml.rb +1 -1
- data/lib/praxis/mapper/active_model_compat.rb +63 -0
- data/lib/praxis/mapper/resource.rb +242 -0
- data/lib/praxis/mapper/selector_generator.rb +126 -0
- data/lib/praxis/mapper/sequel_compat.rb +37 -0
- data/lib/praxis/middleware_app.rb +13 -15
- data/lib/praxis/multipart/part.rb +3 -5
- data/lib/praxis/plugins/mapper_plugin.rb +50 -0
- data/lib/praxis/request.rb +14 -7
- data/lib/praxis/request_stages/response.rb +2 -3
- data/lib/praxis/resource_definition.rb +10 -14
- data/lib/praxis/response.rb +6 -5
- data/lib/praxis/response_definition.rb +5 -7
- data/lib/praxis/response_template.rb +3 -4
- data/lib/praxis/responses/http.rb +36 -0
- data/lib/praxis/responses/internal_server_error.rb +12 -3
- data/lib/praxis/responses/multipart_ok.rb +11 -4
- data/lib/praxis/responses/validation_error.rb +10 -1
- data/lib/praxis/router.rb +3 -3
- data/lib/praxis/tasks/api_docs.rb +2 -10
- data/lib/praxis/tasks/routes.rb +0 -1
- data/lib/praxis/version.rb +1 -1
- data/lib/praxis.rb +13 -9
- data/praxis.gemspec +2 -3
- data/spec/functional_spec.rb +0 -1
- data/spec/praxis/action_definition_spec.rb +15 -26
- data/spec/praxis/api_definition_spec.rb +8 -13
- data/spec/praxis/api_general_info_spec.rb +8 -3
- data/spec/praxis/application_spec.rb +7 -13
- data/spec/praxis/handlers/xml_spec.rb +2 -2
- data/spec/praxis/mapper/resource_spec.rb +169 -0
- data/spec/praxis/mapper/selector_generator_spec.rb +301 -0
- data/spec/praxis/middleware_app_spec.rb +15 -9
- data/spec/praxis/request_spec.rb +7 -17
- data/spec/praxis/request_stages/validate_spec.rb +1 -1
- data/spec/praxis/resource_definition_spec.rb +10 -12
- data/spec/praxis/response_definition_spec.rb +5 -22
- data/spec/praxis/response_spec.rb +5 -12
- data/spec/praxis/responses/internal_server_error_spec.rb +5 -2
- data/spec/praxis/router_spec.rb +4 -8
- data/spec/spec_app/app/models/person.rb +3 -3
- data/spec/spec_app/config/environment.rb +3 -21
- data/spec/spec_app/config.ru +6 -1
- data/spec/spec_helper.rb +2 -17
- data/spec/support/spec_resources.rb +131 -0
- metadata +19 -31
- data/lib/praxis/extensions/attribute_filtering/query_builder.rb +0 -39
- data/lib/praxis/extensions/attribute_filtering.rb +0 -28
- data/lib/praxis/extensions/mapper_selectors.rb +0 -16
- data/lib/praxis/media_type_collection.rb +0 -127
- data/lib/praxis/plugins/praxis_mapper_plugin.rb +0 -246
- data/spec/praxis/media_type_collection_spec.rb +0 -157
- data/spec/praxis/plugins/praxis_mapper_plugin_spec.rb +0 -142
@@ -0,0 +1,126 @@
|
|
1
|
+
module Praxis::Mapper
|
2
|
+
# Generates a set of selectors given a resource and
|
3
|
+
# list of resource attributes.
|
4
|
+
class SelectorGenerator
|
5
|
+
attr_reader :selectors
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@selectors = Hash.new do |hash, key|
|
9
|
+
hash[key] = {select: Set.new, track: Set.new}
|
10
|
+
end
|
11
|
+
@seen = Hash.new do |hash, resource|
|
12
|
+
hash[resource] = Set.new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def add(resource, fields)
|
17
|
+
return if @seen[resource].include? fields
|
18
|
+
@seen[resource] << fields
|
19
|
+
|
20
|
+
fields.each do |name, field|
|
21
|
+
map_property(resource, name, field)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def select_all(resource)
|
26
|
+
selectors[resource.model][:select] = true
|
27
|
+
end
|
28
|
+
|
29
|
+
def map_property(resource, name, fields)
|
30
|
+
if resource.properties.key?(name)
|
31
|
+
add_property(resource, name, fields)
|
32
|
+
elsif resource.model._praxis_associations.key?(name)
|
33
|
+
add_association(resource, name, fields)
|
34
|
+
else
|
35
|
+
add_select(resource, name)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_select(resource, name)
|
40
|
+
return select_all(resource) if name == :*
|
41
|
+
return if selectors[resource.model][:select] == true
|
42
|
+
|
43
|
+
selectors[resource.model][:select] << name
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_track(resource, name)
|
47
|
+
selectors[resource.model][:track] << name
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_association(resource, name, fields)
|
51
|
+
association = resource.model._praxis_associations.fetch(name) do
|
52
|
+
raise "missing association for #{resource} with name #{name}"
|
53
|
+
end
|
54
|
+
associated_resource = resource.model_map[association[:model]]
|
55
|
+
|
56
|
+
case association[:type]
|
57
|
+
when :many_to_one
|
58
|
+
add_track(resource, name)
|
59
|
+
Array(association[:key]).each do |akey|
|
60
|
+
add_select(resource, akey)
|
61
|
+
end
|
62
|
+
when :one_to_many
|
63
|
+
add_track(resource, name)
|
64
|
+
Array(association[:key]).each do |akey|
|
65
|
+
add_select(associated_resource, akey)
|
66
|
+
end
|
67
|
+
when :many_to_many
|
68
|
+
# If we haven't explicitly added the "through" option in the association
|
69
|
+
# then we'll assume the underlying ORM is able to fill in the gap. We will
|
70
|
+
# simply add the fields for the associated resource below
|
71
|
+
if association.key? :through
|
72
|
+
head, *tail = association[:through]
|
73
|
+
new_fields = tail.reverse.inject(fields) do |thing, step|
|
74
|
+
{step => thing}
|
75
|
+
end
|
76
|
+
return add_association(resource, head, new_fields)
|
77
|
+
else
|
78
|
+
add_track(resource, name)
|
79
|
+
end
|
80
|
+
else
|
81
|
+
raise "no select applicable for #{association[:type].inspect}"
|
82
|
+
end
|
83
|
+
|
84
|
+
unless fields == true
|
85
|
+
# recurse into the field
|
86
|
+
add(associated_resource,fields)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def add_property(resource, name, fields)
|
91
|
+
dependencies = resource.properties[name][:dependencies]
|
92
|
+
if dependencies
|
93
|
+
dependencies.each do |dependency|
|
94
|
+
# if dependency includes the name, then map it directly as the field
|
95
|
+
if dependency == name
|
96
|
+
add_select(resource, name)
|
97
|
+
else
|
98
|
+
apply_dependency(resource, dependency)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
head, *tail = resource.properties[name][:through]
|
104
|
+
return if head.nil?
|
105
|
+
|
106
|
+
new_fields = tail.reverse.inject(fields) do |thing, step|
|
107
|
+
{step => thing}
|
108
|
+
end
|
109
|
+
|
110
|
+
add_association(resource, head, new_fields)
|
111
|
+
end
|
112
|
+
|
113
|
+
def apply_dependency(resource, dependency)
|
114
|
+
case dependency
|
115
|
+
when Symbol
|
116
|
+
map_property(resource, dependency, {})
|
117
|
+
when String
|
118
|
+
head, tail = dependency.split('.').collect(&:to_sym)
|
119
|
+
raise "String dependencies can not be singular" if tail.nil?
|
120
|
+
|
121
|
+
add_association(resource, head, {tail => true})
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module Praxis::Mapper
|
4
|
+
module SequelCompat
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
`` included do
|
8
|
+
attr_accessor :_resource
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def _filter_query_builder_class
|
13
|
+
Praxis::Extensions::SequelFilterQueryBuilder
|
14
|
+
end
|
15
|
+
|
16
|
+
def _praxis_associations
|
17
|
+
orig = self.association_reflections.clone
|
18
|
+
|
19
|
+
orig.each do |k,v|
|
20
|
+
v[:model] = v.associated_class
|
21
|
+
if v.respond_to?(:primary_key)
|
22
|
+
v[:primary_key] = v.primary_key
|
23
|
+
else
|
24
|
+
# FIXME: figure out exactly what to do here.
|
25
|
+
# not super critical, as we can't track these associations
|
26
|
+
# directly, but it would be nice to traverse these
|
27
|
+
# properly.
|
28
|
+
v[:primary_key] = :unsupported
|
29
|
+
end
|
30
|
+
end
|
31
|
+
orig
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -2,39 +2,37 @@ module Praxis
|
|
2
2
|
class MiddlewareApp
|
3
3
|
|
4
4
|
attr_reader :target
|
5
|
+
|
5
6
|
# Initialize the application instance with the desired args, and return the wrapping class.
|
6
7
|
def self.for( **args )
|
7
8
|
Class.new(self) do
|
8
|
-
class << self
|
9
|
-
attr_accessor :app_instance
|
10
|
-
attr_reader :app_name, :skip_registration
|
11
|
-
end
|
12
|
-
@app_name = args.delete(:name)
|
13
|
-
@skip_registration = args.delete(:skip_registration) || false
|
14
9
|
@args = args
|
15
|
-
@
|
16
|
-
|
10
|
+
@setup_done = false
|
17
11
|
def self.name
|
18
12
|
'MiddlewareApp'
|
19
13
|
end
|
20
14
|
def self.args
|
21
15
|
@args
|
22
16
|
end
|
17
|
+
def self.setup_done
|
18
|
+
@setup_done
|
19
|
+
end
|
23
20
|
def self.setup
|
24
|
-
|
21
|
+
@setup_done = true
|
22
|
+
Praxis::Application.instance.setup(**@args)
|
25
23
|
end
|
26
24
|
end
|
27
|
-
|
25
|
+
end
|
28
26
|
|
29
27
|
def initialize( inner )
|
30
28
|
@target = inner
|
31
|
-
|
29
|
+
@setup_done = false
|
32
30
|
end
|
33
|
-
|
31
|
+
|
34
32
|
def call(env)
|
35
|
-
|
36
|
-
|
37
|
-
result =
|
33
|
+
self.class.setup unless self.class.setup_done
|
34
|
+
|
35
|
+
result = Praxis::Application.instance.call(env)
|
38
36
|
|
39
37
|
unless ( [404,405].include?(result[0].to_i) && result[1]['X-Cascade'] == 'pass' )
|
40
38
|
# Respect X-Cascade header if it doesn't specify 'pass'
|
@@ -10,7 +10,6 @@ module Praxis
|
|
10
10
|
attr_accessor :headers_attribute
|
11
11
|
attr_accessor :filename_attribute
|
12
12
|
attr_accessor :default_handler
|
13
|
-
attr_accessor :application
|
14
13
|
|
15
14
|
def self.check_option!(name, definition)
|
16
15
|
case name
|
@@ -78,8 +77,7 @@ module Praxis
|
|
78
77
|
@name = name
|
79
78
|
@body = body
|
80
79
|
@headers = headers
|
81
|
-
@
|
82
|
-
@default_handler = application.handlers['json']
|
80
|
+
@default_handler = Praxis::Application.instance.handlers['json']
|
83
81
|
|
84
82
|
if content_type.nil?
|
85
83
|
self.content_type = 'text/plain'
|
@@ -214,7 +212,7 @@ module Praxis
|
|
214
212
|
end
|
215
213
|
|
216
214
|
def handler
|
217
|
-
handlers =
|
215
|
+
handlers = Praxis::Application.instance.handlers
|
218
216
|
(content_type && handlers[content_type.handler_name]) || @default_handler
|
219
217
|
end
|
220
218
|
|
@@ -251,7 +249,7 @@ module Praxis
|
|
251
249
|
|
252
250
|
# and return that one if it already corresponds to a registered handler
|
253
251
|
# otherwise, add the encoding
|
254
|
-
if
|
252
|
+
if Praxis::Application.instance.handlers.include?(pick.handler_name)
|
255
253
|
return pick
|
256
254
|
else
|
257
255
|
return pick + handler_name
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'praxis/extensions/attribute_filtering/filtering_params'
|
3
|
+
|
4
|
+
module Praxis
|
5
|
+
module Plugins
|
6
|
+
module MapperPlugin
|
7
|
+
include Praxis::PluginConcern
|
8
|
+
|
9
|
+
class Plugin < Praxis::Plugin
|
10
|
+
include Singleton
|
11
|
+
end
|
12
|
+
|
13
|
+
module Controller
|
14
|
+
extend ActiveSupport::Concern
|
15
|
+
|
16
|
+
included do
|
17
|
+
include Praxis::Extensions::FieldExpansion
|
18
|
+
end
|
19
|
+
|
20
|
+
def set_selectors
|
21
|
+
return unless self.media_type.respond_to?(:domain_model) &&
|
22
|
+
self.media_type.domain_model < Praxis::Mapper::Resource
|
23
|
+
|
24
|
+
resolved = Praxis::MediaType::FieldResolver.resolve(self.media_type, self.expanded_fields)
|
25
|
+
selector_generator.add(self.media_type.domain_model, resolved)
|
26
|
+
end
|
27
|
+
|
28
|
+
def build_query(base_query) # rubocop:disable Metrics/AbcSize
|
29
|
+
domain_model = self.media_type&.domain_model
|
30
|
+
raise "No domain model defined for #{self.name}. Cannot use the attribute filtering helpers without it" unless domain_model
|
31
|
+
|
32
|
+
filters = request.params.filters if request.params&.respond_to?(:filters)
|
33
|
+
base_query = domain_model.craft_filter_query( base_query , filters: filters )
|
34
|
+
|
35
|
+
resolved = Praxis::MediaType::FieldResolver.resolve(self.media_type, self.expanded_fields)
|
36
|
+
base_query = domain_model.craft_field_selection_query(base_query, selectors: selector_generator.selectors, resolved: resolved)
|
37
|
+
|
38
|
+
# TODO: handle pagination and ordering
|
39
|
+
base_query
|
40
|
+
end
|
41
|
+
|
42
|
+
def selector_generator
|
43
|
+
@selector_generator ||= Praxis::Mapper::SelectorGenerator.new
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/praxis/request.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Praxis
|
2
2
|
|
3
3
|
class Request < Praxis.request_superclass
|
4
|
-
attr_reader :env, :query
|
4
|
+
attr_reader :env, :query
|
5
5
|
attr_accessor :route_params, :action
|
6
6
|
|
7
7
|
PATH_VERSION_PREFIX = "/v".freeze
|
@@ -14,11 +14,10 @@ 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)
|
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]
|
22
21
|
end
|
23
22
|
|
24
23
|
# Determine the content type of this request as indicated by the Content-Type header.
|
@@ -87,8 +86,8 @@ module Praxis
|
|
87
86
|
PATH_VERSION_MATCHER = %r{^#{self.path_version_prefix}(?<version>[^\/]+)\/}.freeze
|
88
87
|
|
89
88
|
def path_version_matcher
|
90
|
-
if
|
91
|
-
matcher = Mustermann.new(
|
89
|
+
if Application.instance.versioning_scheme == :path
|
90
|
+
matcher = Mustermann.new(ApiDefinition.instance.info.base_path + '*')
|
92
91
|
matcher.params(self.path)[API_VERSION_PARAM_NAME]
|
93
92
|
else
|
94
93
|
PATH_VERSION_MATCHER.match(self.path)[:version]
|
@@ -97,7 +96,8 @@ module Praxis
|
|
97
96
|
|
98
97
|
def version
|
99
98
|
result = nil
|
100
|
-
|
99
|
+
|
100
|
+
Array(Application.instance.versioning_scheme).find do |mode|
|
101
101
|
case mode
|
102
102
|
when :header;
|
103
103
|
result = env[API_VERSION_HEADER_NAME]
|
@@ -128,7 +128,8 @@ module Praxis
|
|
128
128
|
def load_payload(context)
|
129
129
|
return unless action.payload
|
130
130
|
return if content_type.nil?
|
131
|
-
|
131
|
+
|
132
|
+
raw = if (handler = Praxis::Application.instance.handlers[content_type.handler_name])
|
132
133
|
handler.parse(self.raw_payload)
|
133
134
|
else
|
134
135
|
# TODO is this a good default?
|
@@ -161,6 +162,12 @@ module Praxis
|
|
161
162
|
@unmatched_versions ||= Set.new
|
162
163
|
end
|
163
164
|
|
165
|
+
# Override the inspect instance method of a request, as, by default, the kernel inspect will go nuts
|
166
|
+
# traversing the action and app_instance and therefore all associated instance variables reachable through that
|
167
|
+
def inspect
|
168
|
+
"'@env' => #{@env.inspect},\n'@headers' => #{@headers.inspect},\n'@params' => #{@params.inspect},\n'@query' => #{@query.inspect}"
|
169
|
+
end
|
170
|
+
|
164
171
|
end
|
165
172
|
|
166
173
|
end
|
@@ -8,9 +8,8 @@ module Praxis
|
|
8
8
|
|
9
9
|
response.handle
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
validate_body = config.praxis.validate_response_bodies
|
11
|
+
if Application.instance.config.praxis.validate_responses == true
|
12
|
+
validate_body = Application.instance.config.praxis.validate_response_bodies
|
14
13
|
|
15
14
|
response.validate(action, validate_body: validate_body)
|
16
15
|
end
|
@@ -8,15 +8,11 @@ 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
|
-
|
15
11
|
@version = 'n/a'.freeze
|
16
12
|
@actions = Hash.new
|
17
13
|
@responses = Hash.new
|
18
14
|
|
19
|
-
@action_defaults = Trait.new &ResourceDefinition.generate_defaults_block
|
15
|
+
@action_defaults = Trait.new &ResourceDefinition.generate_defaults_block
|
20
16
|
|
21
17
|
@version_options = {}
|
22
18
|
@metadata = {}
|
@@ -37,12 +33,13 @@ module Praxis
|
|
37
33
|
|
38
34
|
@on_finalize = Array.new
|
39
35
|
|
40
|
-
|
36
|
+
Application.instance.resource_definitions << self
|
41
37
|
end
|
42
38
|
|
43
|
-
def self.generate_defaults_block( version: nil
|
39
|
+
def self.generate_defaults_block( version: nil )
|
40
|
+
|
44
41
|
# Ensure we inherit any base params defined in the API definition for the passed in version
|
45
|
-
base_attributes = if (base_params =
|
42
|
+
base_attributes = if (base_params = ApiDefinition.instance.info(version).base_params)
|
46
43
|
base_params.attributes
|
47
44
|
else
|
48
45
|
{}
|
@@ -59,8 +56,8 @@ module Praxis
|
|
59
56
|
end
|
60
57
|
end
|
61
58
|
|
62
|
-
def self.finalize!
|
63
|
-
|
59
|
+
def self.finalize!
|
60
|
+
Application.instance.resource_definitions.each do |resource_definition|
|
64
61
|
while (block = resource_definition.on_finalize.shift)
|
65
62
|
block.call
|
66
63
|
end
|
@@ -76,7 +73,6 @@ module Praxis
|
|
76
73
|
attr_reader :traits
|
77
74
|
attr_reader :version_prefix
|
78
75
|
attr_reader :parent_prefix
|
79
|
-
attr_reader :application
|
80
76
|
|
81
77
|
# opaque hash of user-defined medata, used to decorate the definition,
|
82
78
|
# and also available in the generated JSON documents
|
@@ -203,7 +199,7 @@ module Praxis
|
|
203
199
|
end
|
204
200
|
end
|
205
201
|
|
206
|
-
@action_defaults.instance_eval &ResourceDefinition.generate_defaults_block( version: version
|
202
|
+
@action_defaults.instance_eval &ResourceDefinition.generate_defaults_block( version: version )
|
207
203
|
end
|
208
204
|
|
209
205
|
|
@@ -242,10 +238,10 @@ module Praxis
|
|
242
238
|
end
|
243
239
|
|
244
240
|
def trait(trait_name)
|
245
|
-
unless
|
241
|
+
unless ApiDefinition.instance.traits.has_key? trait_name
|
246
242
|
raise Exceptions::InvalidTrait.new("Trait #{trait_name} not found in the system")
|
247
243
|
end
|
248
|
-
trait =
|
244
|
+
trait = ApiDefinition.instance.traits.fetch(trait_name)
|
249
245
|
@traits << trait_name
|
250
246
|
end
|
251
247
|
alias_method :use, :trait
|
data/lib/praxis/response.rb
CHANGED
@@ -63,23 +63,24 @@ module Praxis
|
|
63
63
|
self.class.response_name
|
64
64
|
end
|
65
65
|
|
66
|
-
def format!
|
66
|
+
def format!
|
67
67
|
end
|
68
68
|
|
69
|
-
def encode!
|
69
|
+
def encode!
|
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
|
75
76
|
handler = (content_type && handlers[content_type.handler_name]) || handlers['json']
|
76
77
|
@body = handler.generate(@body)
|
77
78
|
end
|
78
79
|
end
|
79
80
|
|
80
|
-
def finish
|
81
|
-
format!
|
82
|
-
encode!
|
81
|
+
def finish
|
82
|
+
format!
|
83
|
+
encode!
|
83
84
|
|
84
85
|
@body = Array(@body)
|
85
86
|
|
@@ -4,9 +4,8 @@ module Praxis
|
|
4
4
|
|
5
5
|
class ResponseDefinition
|
6
6
|
attr_reader :name
|
7
|
-
|
8
|
-
|
9
|
-
def initialize(response_name, application, **spec, &block)
|
7
|
+
|
8
|
+
def initialize(response_name, **spec, &block)
|
10
9
|
unless response_name
|
11
10
|
raise Exceptions::InvalidConfiguration.new(
|
12
11
|
"Response name is required for a response specification"
|
@@ -14,7 +13,6 @@ module Praxis
|
|
14
13
|
end
|
15
14
|
@spec = { headers:{} }
|
16
15
|
@name = response_name
|
17
|
-
@application = application
|
18
16
|
self.instance_exec(**spec, &block) if block_given?
|
19
17
|
|
20
18
|
if self.status.nil?
|
@@ -143,9 +141,9 @@ module Praxis
|
|
143
141
|
# FIXME: remove load when when MediaTypeCommon.identifier returns a MediaTypeIdentifier
|
144
142
|
identifier = MediaTypeIdentifier.load(self.media_type.identifier)
|
145
143
|
|
146
|
-
default_handlers =
|
144
|
+
default_handlers = ApiDefinition.instance.info.produces
|
147
145
|
|
148
|
-
handlers =
|
146
|
+
handlers = Praxis::Application.instance.handlers.select do |k,v|
|
149
147
|
default_handlers.include?(k)
|
150
148
|
end
|
151
149
|
|
@@ -202,7 +200,7 @@ module Praxis
|
|
202
200
|
raise ArgumentError, "Parts definition for response #{name} does not allow :like and a block simultaneously"
|
203
201
|
end
|
204
202
|
if like
|
205
|
-
template =
|
203
|
+
template = ApiDefinition.instance.response(like)
|
206
204
|
@parts = template.compile(nil, **args)
|
207
205
|
else # block
|
208
206
|
@parts = Praxis::ResponseDefinition.new('anonymous', **args, &a_proc)
|
@@ -1,12 +1,11 @@
|
|
1
1
|
module Praxis
|
2
2
|
|
3
3
|
class ResponseTemplate
|
4
|
-
attr_reader :name, :block
|
4
|
+
attr_reader :name, :block
|
5
5
|
|
6
|
-
def initialize(response_name,
|
6
|
+
def initialize(response_name, &block)
|
7
7
|
@name = response_name
|
8
8
|
@block = block
|
9
|
-
@application = application
|
10
9
|
end
|
11
10
|
|
12
11
|
def compile(action=nil, **args)
|
@@ -24,7 +23,7 @@ module Praxis
|
|
24
23
|
args[:media_type] = media_type
|
25
24
|
end
|
26
25
|
end
|
27
|
-
Praxis::ResponseDefinition.new(name,
|
26
|
+
Praxis::ResponseDefinition.new(name, **args, &block)
|
28
27
|
end
|
29
28
|
|
30
29
|
def describe
|
@@ -131,5 +131,41 @@ 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
|
+
|
134
170
|
end
|
135
171
|
end
|
@@ -13,15 +13,16 @@ module Praxis
|
|
13
13
|
@error = error
|
14
14
|
end
|
15
15
|
|
16
|
-
def format!(exception = @error
|
16
|
+
def format!(exception = @error)
|
17
17
|
if @error
|
18
|
-
|
18
|
+
|
19
|
+
if Application.instance.config.praxis.show_exceptions == true
|
19
20
|
msg = {
|
20
21
|
name: exception.class.name,
|
21
22
|
message: exception.message,
|
22
23
|
backtrace: exception.backtrace
|
23
24
|
}
|
24
|
-
msg[:cause] = format!(exception.cause
|
25
|
+
msg[:cause] = format!(exception.cause) if exception.cause
|
25
26
|
else
|
26
27
|
msg = {name: 'InternalServerError', message: "Something bad happened."}
|
27
28
|
end
|
@@ -33,4 +34,12 @@ module Praxis
|
|
33
34
|
|
34
35
|
end
|
35
36
|
|
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
|
+
|
36
45
|
end
|
@@ -19,7 +19,7 @@ module Praxis
|
|
19
19
|
end
|
20
20
|
|
21
21
|
|
22
|
-
def encode!
|
22
|
+
def encode!
|
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
|
32
|
+
format!
|
33
|
+
encode!
|
34
34
|
|
35
35
|
@body = Array(@body)
|
36
36
|
|
@@ -41,4 +41,11 @@ 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
|
+
|
44
51
|
end
|
@@ -15,7 +15,7 @@ module Praxis
|
|
15
15
|
@documentation = documentation
|
16
16
|
end
|
17
17
|
|
18
|
-
def format!
|
18
|
+
def format!
|
19
19
|
@body = {name: 'ValidationError', summary: @summary }
|
20
20
|
@body[:errors] = @errors if @errors
|
21
21
|
|
@@ -31,4 +31,13 @@ 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
|
+
|
34
43
|
end
|