praxis 2.0.pre.17 → 2.0.pre.21
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/.rubocop.yml +54 -0
- data/.simplecov +3 -1
- data/.travis.yml +2 -1
- data/CHANGELOG.md +19 -0
- data/CONTRIBUTING.md +2 -79
- data/Gemfile +5 -1
- data/Guardfile +6 -4
- data/LICENSE +0 -2
- data/MAINTAINERS.md +1 -0
- data/README.md +15 -22
- data/Rakefile +4 -2
- data/bin/praxis +55 -58
- data/lib/praxis/action_definition/headers_dsl_compiler.rb +5 -6
- data/lib/praxis/action_definition.rb +65 -95
- data/lib/praxis/api_definition.rb +21 -29
- data/lib/praxis/api_general_info.rb +55 -66
- data/lib/praxis/application.rb +15 -32
- data/lib/praxis/blueprint.rb +80 -73
- data/lib/praxis/bootloader.rb +24 -33
- data/lib/praxis/bootloader_stages/environment.rb +5 -10
- data/lib/praxis/bootloader_stages/file_loader.rb +3 -6
- data/lib/praxis/bootloader_stages/plugin_config_load.rb +4 -6
- data/lib/praxis/bootloader_stages/plugin_config_prepare.rb +2 -2
- data/lib/praxis/bootloader_stages/plugin_loader.rb +3 -7
- data/lib/praxis/bootloader_stages/plugin_setup.rb +3 -3
- data/lib/praxis/bootloader_stages/routing.rb +5 -8
- data/lib/praxis/bootloader_stages/subgroup_loader.rb +2 -10
- data/lib/praxis/bootloader_stages/warn_unloaded_files.rb +15 -19
- data/lib/praxis/callbacks.rb +12 -11
- data/lib/praxis/collection.rb +11 -14
- data/lib/praxis/config.rb +17 -28
- data/lib/praxis/config_hash.rb +2 -1
- data/lib/praxis/controller.rb +7 -6
- data/lib/praxis/dispatcher.rb +34 -42
- data/lib/praxis/docs/open_api/info_object.rb +11 -8
- data/lib/praxis/docs/open_api/media_type_object.rb +18 -17
- data/lib/praxis/docs/open_api/operation_object.rb +7 -4
- data/lib/praxis/docs/open_api/parameter_object.rb +17 -14
- data/lib/praxis/docs/open_api/paths_object.rb +11 -9
- data/lib/praxis/docs/open_api/request_body_object.rb +14 -13
- data/lib/praxis/docs/open_api/response_object.rb +24 -18
- data/lib/praxis/docs/open_api/responses_object.rb +3 -1
- data/lib/praxis/docs/open_api/schema_object.rb +61 -29
- data/lib/praxis/docs/open_api/server_object.rb +5 -2
- data/lib/praxis/docs/open_api/tag_object.rb +9 -6
- data/lib/praxis/docs/open_api_generator.rb +114 -150
- data/lib/praxis/endpoint_definition.rb +60 -77
- data/lib/praxis/error_handler.rb +2 -2
- data/lib/praxis/exception.rb +2 -0
- data/lib/praxis/exceptions/config.rb +3 -1
- data/lib/praxis/exceptions/config_load.rb +2 -0
- data/lib/praxis/exceptions/config_validation.rb +3 -1
- data/lib/praxis/exceptions/invalid_configuration.rb +3 -1
- data/lib/praxis/exceptions/invalid_response.rb +3 -1
- data/lib/praxis/exceptions/invalid_trait.rb +3 -1
- data/lib/praxis/exceptions/stage_not_found.rb +3 -1
- data/lib/praxis/exceptions/validation.rb +4 -3
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +163 -149
- data/lib/praxis/extensions/attribute_filtering/active_record_patches/5x.rb +18 -13
- data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_0.rb +13 -9
- data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_1_plus.rb +14 -11
- data/lib/praxis/extensions/attribute_filtering/active_record_patches.rb +12 -9
- data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +8 -5
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +89 -65
- data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +68 -62
- data/lib/praxis/extensions/attribute_filtering.rb +3 -1
- data/lib/praxis/extensions/field_expansion.rb +6 -4
- data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +10 -8
- data/lib/praxis/extensions/field_selection/field_selector.rb +91 -92
- data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +12 -12
- data/lib/praxis/extensions/field_selection.rb +3 -1
- data/lib/praxis/extensions/pagination/active_record_pagination_handler.rb +6 -4
- data/lib/praxis/extensions/pagination/header_generator.rb +16 -11
- data/lib/praxis/extensions/pagination/ordering_params.rb +29 -28
- data/lib/praxis/extensions/pagination/pagination_handler.rb +44 -42
- data/lib/praxis/extensions/pagination/pagination_params.rb +29 -48
- data/lib/praxis/extensions/pagination/sequel_pagination_handler.rb +8 -7
- data/lib/praxis/extensions/pagination.rb +10 -15
- data/lib/praxis/extensions/rails_compat/request_methods.rb +3 -4
- data/lib/praxis/extensions/rails_compat.rb +2 -0
- data/lib/praxis/extensions/rendering.rb +12 -12
- data/lib/praxis/field_expander.rb +8 -9
- data/lib/praxis/file_group.rb +8 -12
- data/lib/praxis/finalizable.rb +1 -0
- data/lib/praxis/handlers/json.rb +5 -2
- data/lib/praxis/handlers/plain.rb +2 -1
- data/lib/praxis/handlers/www_form.rb +6 -3
- data/lib/praxis/handlers/{xml-sample.rb → xml_sample.rb} +26 -22
- data/lib/praxis/mapper/active_model_compat.rb +13 -10
- data/lib/praxis/mapper/resource.rb +196 -181
- data/lib/praxis/mapper/selector_generator.rb +106 -112
- data/lib/praxis/mapper/sequel_compat.rb +70 -67
- data/lib/praxis/media_type.rb +2 -2
- data/lib/praxis/media_type_identifier.rb +26 -22
- data/lib/praxis/middleware_app.rb +18 -15
- data/lib/praxis/multipart/parser.rb +46 -51
- data/lib/praxis/multipart/part.rb +78 -110
- data/lib/praxis/notifications.rb +2 -4
- data/lib/praxis/plugin.rb +11 -18
- data/lib/praxis/plugin_concern.rb +12 -15
- data/lib/praxis/plugins/mapper_plugin.rb +15 -13
- data/lib/praxis/plugins/pagination_plugin.rb +8 -6
- data/lib/praxis/plugins/rails_plugin.rb +33 -28
- data/lib/praxis/renderer.rb +11 -15
- data/lib/praxis/request.rb +48 -44
- data/lib/praxis/request_stages/action.rb +4 -6
- data/lib/praxis/request_stages/load_request.rb +2 -4
- data/lib/praxis/request_stages/request_stage.rb +19 -23
- data/lib/praxis/request_stages/response.rb +4 -6
- data/lib/praxis/request_stages/validate.rb +3 -5
- data/lib/praxis/request_stages/validate_params_and_headers.rb +15 -22
- data/lib/praxis/request_stages/validate_payload.rb +25 -28
- data/lib/praxis/request_superclassing.rb +3 -3
- data/lib/praxis/resource_definition.rb +1 -0
- data/lib/praxis/response.rb +24 -26
- data/lib/praxis/response_definition.rb +77 -122
- data/lib/praxis/response_template.rb +11 -15
- data/lib/praxis/responses/http.rb +23 -44
- data/lib/praxis/responses/internal_server_error.rb +18 -21
- data/lib/praxis/responses/multipart_ok.rb +4 -9
- data/lib/praxis/responses/validation_error.rb +8 -15
- data/lib/praxis/route.rb +8 -10
- data/lib/praxis/router/rack.rb +13 -7
- data/lib/praxis/router/simple.rb +10 -5
- data/lib/praxis/router.rb +27 -34
- data/lib/praxis/routing_config.rb +52 -29
- data/lib/praxis/simple_media_type.rb +5 -8
- data/lib/praxis/stage.rb +17 -25
- data/lib/praxis/tasks/api_docs.rb +17 -16
- data/lib/praxis/tasks/console.rb +3 -1
- data/lib/praxis/tasks/environment.rb +2 -0
- data/lib/praxis/tasks/routes.rb +26 -24
- data/lib/praxis/tasks.rb +3 -1
- data/lib/praxis/trait.rb +37 -46
- data/lib/praxis/types/fuzzy_hash.rb +13 -14
- data/lib/praxis/types/media_type_common.rb +11 -10
- data/lib/praxis/types/multipart_array/part_definition.rb +14 -17
- data/lib/praxis/types/multipart_array.rb +100 -115
- data/lib/praxis/validation_handler.rb +5 -3
- data/lib/praxis/version.rb +3 -1
- data/lib/praxis.rb +4 -5
- data/praxis.gemspec +22 -21
- data/spec/functional_spec.rb +44 -56
- data/spec/praxis/action_definition_spec.rb +39 -48
- data/spec/praxis/api_definition_spec.rb +45 -47
- data/spec/praxis/api_general_info_spec.rb +28 -29
- data/spec/praxis/application_spec.rb +18 -14
- data/spec/praxis/blueprint_spec.rb +33 -34
- data/spec/praxis/bootloader_spec.rb +32 -30
- data/spec/praxis/callbacks_spec.rb +37 -37
- data/spec/praxis/collection_spec.rb +18 -25
- data/spec/praxis/config_hash_spec.rb +5 -4
- data/spec/praxis/config_spec.rb +27 -26
- data/spec/praxis/controller_spec.rb +8 -9
- data/spec/praxis/endpoint_definition_spec.rb +25 -32
- data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +171 -114
- data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +22 -21
- data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +112 -60
- data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +37 -38
- data/spec/praxis/extensions/field_expansion_spec.rb +8 -10
- data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +14 -13
- data/spec/praxis/extensions/field_selection/field_selector_spec.rb +9 -16
- data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +50 -49
- data/spec/praxis/extensions/pagination/active_record_pagination_handler_spec.rb +32 -31
- data/spec/praxis/extensions/rendering_spec.rb +9 -9
- data/spec/praxis/extensions/support/spec_resources_active_model.rb +32 -49
- data/spec/praxis/extensions/support/spec_resources_sequel.rb +48 -48
- data/spec/praxis/field_expander_spec.rb +6 -5
- data/spec/praxis/file_group_spec.rb +3 -1
- data/spec/praxis/handlers/json_spec.rb +6 -5
- data/spec/praxis/mapper/resource_spec.rb +39 -29
- data/spec/praxis/mapper/selector_generator_spec.rb +80 -46
- data/spec/praxis/media_type_identifier_spec.rb +13 -10
- data/spec/praxis/media_type_spec.rb +12 -12
- data/spec/praxis/middleware_app_spec.rb +23 -22
- data/spec/praxis/multipart/parser_spec.rb +7 -9
- data/spec/praxis/notifications_spec.rb +4 -4
- data/spec/praxis/plugin_concern_spec.rb +5 -6
- data/spec/praxis/renderer_spec.rb +10 -9
- data/spec/praxis/request_spec.rb +38 -41
- data/spec/praxis/request_stages/action_spec.rb +14 -15
- data/spec/praxis/request_stages/request_stage_spec.rb +30 -41
- data/spec/praxis/request_stages/validate_spec.rb +3 -1
- data/spec/praxis/response_definition_spec.rb +79 -92
- data/spec/praxis/response_spec.rb +35 -40
- data/spec/praxis/responses/internal_server_error_spec.rb +6 -9
- data/spec/praxis/responses/validation_error_spec.rb +17 -18
- data/spec/praxis/route_spec.rb +4 -7
- data/spec/praxis/router_spec.rb +69 -79
- data/spec/praxis/routing_config_spec.rb +15 -14
- data/spec/praxis/stage_spec.rb +56 -53
- data/spec/praxis/trait_spec.rb +17 -17
- data/spec/praxis/types/fuzzy_hash_spec.rb +11 -9
- data/spec/praxis/types/multipart_array/part_definition_spec.rb +3 -2
- data/spec/praxis/types/multipart_array_spec.rb +33 -48
- data/spec/spec_app/app/concerns/authenticated.rb +5 -5
- data/spec/spec_app/app/concerns/basic_api.rb +3 -1
- data/spec/spec_app/app/concerns/log_wrapper.rb +5 -3
- data/spec/spec_app/app/controllers/base_class.rb +6 -5
- data/spec/spec_app/app/controllers/instances.rb +31 -34
- data/spec/spec_app/app/controllers/volumes.rb +6 -6
- data/spec/spec_app/app/responses/multipart.rb +1 -2
- data/spec/spec_app/app/responses/other_response.rb +2 -2
- data/spec/spec_app/config/environment.rb +19 -6
- data/spec/spec_app/config.ru +4 -3
- data/spec/spec_app/design/api.rb +13 -15
- data/spec/spec_app/design/media_types/instance.rb +6 -6
- data/spec/spec_app/design/media_types/volume.rb +2 -1
- data/spec/spec_app/design/media_types/volume_snapshot.rb +2 -1
- data/spec/spec_app/design/resources/instances.rb +11 -17
- data/spec/spec_app/design/resources/volume_snapshots.rb +4 -5
- data/spec/spec_app/design/resources/volumes.rb +4 -5
- data/spec/spec_helper.rb +11 -13
- data/spec/support/be_deep_equal_matcher.rb +5 -0
- data/spec/support/spec_authorization_plugin.rb +7 -12
- data/spec/support/spec_blueprints.rb +5 -4
- data/spec/support/spec_complex_authentication_plugin.rb +17 -34
- data/spec/support/spec_endpoint_definitions.rb +2 -3
- data/spec/support/spec_media_types.rb +28 -35
- data/spec/support/spec_resources.rb +22 -16
- data/spec/support/spec_simple_authentication_plugin.rb +5 -9
- data/tasks/loader.thor +4 -2
- data/tasks/thor/app.rb +7 -5
- data/tasks/thor/example.rb +23 -22
- data/tasks/thor/model.rb +7 -7
- data/tasks/thor/scaffold.rb +23 -23
- data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +0 -8
- data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +1 -2
- metadata +72 -84
- data/MAINTAINERS +0 -2
- data/TODO.md +0 -25
- data/spec/praxis/api_resource_spec.rb +0 -0
- data/spec/praxis/dispatcher_spec.rb +0 -0
- data/spec/spec_app/app/responses/bulk_response.rb +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Praxis
|
|
2
4
|
module Extensions
|
|
3
5
|
module Pagination
|
|
4
6
|
class PaginationHandler
|
|
5
|
-
class PaginationException <
|
|
7
|
+
class PaginationException < RuntimeError; end
|
|
6
8
|
|
|
7
9
|
def self.paginate(query, pagination)
|
|
8
10
|
return query unless pagination.paginator
|
|
@@ -10,57 +12,57 @@ module Praxis
|
|
|
10
12
|
paginator = pagination.paginator
|
|
11
13
|
|
|
12
14
|
q = if paginator.page
|
|
13
|
-
|
|
14
|
-
else
|
|
15
|
-
# If there is an order clause that complies with the "by" field sorting, we can use it directly
|
|
16
|
-
# i.e., We can be smart about allowing the main sort field matching the pagination one (in case you want to sub-order in a custom way)
|
|
17
|
-
oclause = if pagination.order.nil? || pagination.order.empty? # No ordering specified => use ascending based on the "by" field
|
|
18
|
-
direction = :asc
|
|
19
|
-
order(query, [{ asc: paginator.by }])
|
|
20
|
-
else
|
|
21
|
-
first_ordering = pagination.order.items.first
|
|
22
|
-
direction = first_ordering.keys.first
|
|
23
|
-
unless first_ordering[direction].to_sym == pagination.paginator.by.to_sym
|
|
24
|
-
string_clause = pagination.order.items.map { |h|
|
|
25
|
-
dir, name = h.first
|
|
26
|
-
"#{name} #{dir}"
|
|
27
|
-
}.join(',')
|
|
28
|
-
raise PaginationException,
|
|
29
|
-
"Ordering clause [#{string_clause}] is incompatible with pagination by field '#{pagination.paginator.by}'. " \
|
|
30
|
-
"When paginating by a field value, one cannot specify the 'order' clause " \
|
|
31
|
-
"unless the clause's primary field matches the pagination field."
|
|
32
|
-
end
|
|
33
|
-
order(query, pagination.order)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
if paginator.from
|
|
37
|
-
if direction == :desc
|
|
38
|
-
where_lt(oclause, paginator.by, paginator.from)
|
|
15
|
+
offset(query, (paginator.page - 1) * paginator.items)
|
|
39
16
|
else
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
17
|
+
# If there is an order clause that complies with the "by" field sorting, we can use it directly
|
|
18
|
+
# i.e., We can be smart about allowing the main sort field matching the pagination one (in case you want to sub-order in a custom way)
|
|
19
|
+
oclause = if pagination.order.nil? || pagination.order.empty? # No ordering specified => use ascending based on the "by" field
|
|
20
|
+
direction = :asc
|
|
21
|
+
order(query, [{ asc: paginator.by }])
|
|
22
|
+
else
|
|
23
|
+
first_ordering = pagination.order.items.first
|
|
24
|
+
direction = first_ordering.keys.first
|
|
25
|
+
unless first_ordering[direction].to_sym == pagination.paginator.by.to_sym
|
|
26
|
+
string_clause = pagination.order.items.map do |h|
|
|
27
|
+
dir, name = h.first
|
|
28
|
+
"#{name} #{dir}"
|
|
29
|
+
end.join(',')
|
|
30
|
+
raise PaginationException,
|
|
31
|
+
"Ordering clause [#{string_clause}] is incompatible with pagination by field '#{pagination.paginator.by}'. " \
|
|
32
|
+
"When paginating by a field value, one cannot specify the 'order' clause " \
|
|
33
|
+
"unless the clause's primary field matches the pagination field."
|
|
34
|
+
end
|
|
35
|
+
order(query, pagination.order)
|
|
36
|
+
end
|
|
45
37
|
|
|
46
|
-
|
|
38
|
+
if paginator.from
|
|
39
|
+
if direction == :desc
|
|
40
|
+
where_lt(oclause, paginator.by, paginator.from)
|
|
41
|
+
else
|
|
42
|
+
where_gt(oclause, paginator.by, paginator.from)
|
|
43
|
+
end
|
|
44
|
+
else
|
|
45
|
+
oclause
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
end
|
|
47
49
|
limit(q, paginator.items)
|
|
48
50
|
end
|
|
49
51
|
|
|
50
|
-
def self.where_lt(
|
|
51
|
-
raise
|
|
52
|
+
def self.where_lt(_query, _attr, _value)
|
|
53
|
+
raise 'implement in derived class'
|
|
52
54
|
end
|
|
53
|
-
|
|
54
|
-
def self.where_gt(
|
|
55
|
-
raise
|
|
55
|
+
|
|
56
|
+
def self.where_gt(_query, _attr, _value)
|
|
57
|
+
raise 'implement in derived class'
|
|
56
58
|
end
|
|
57
59
|
|
|
58
|
-
def self.offset(
|
|
59
|
-
raise
|
|
60
|
+
def self.offset(_query, _offset)
|
|
61
|
+
raise 'implement in derived class'
|
|
60
62
|
end
|
|
61
63
|
|
|
62
|
-
def self.limit(
|
|
63
|
-
raise
|
|
64
|
+
def self.limit(_query, _limit)
|
|
65
|
+
raise 'implement in derived class'
|
|
64
66
|
end
|
|
65
67
|
end
|
|
66
68
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Praxis
|
|
2
4
|
module Extensions
|
|
3
5
|
module Pagination
|
|
@@ -76,40 +78,32 @@ module Praxis
|
|
|
76
78
|
default_mode, default_value = target.defaults[:default_mode].first
|
|
77
79
|
case pagination_type
|
|
78
80
|
when :paging
|
|
79
|
-
if default_mode == :page
|
|
80
|
-
|
|
81
|
-
end
|
|
81
|
+
raise "Cannot disallow page-based pagination if you define a default pagination of: page: #{default_value}" if default_mode == :page
|
|
82
|
+
|
|
82
83
|
target.defaults[:disallow_paging] = true
|
|
83
84
|
when :cursor
|
|
84
|
-
if default_mode == :by
|
|
85
|
-
|
|
86
|
-
end
|
|
85
|
+
raise "Cannot disallow cursor-based pagination if you define a default pagination of: by: #{default_value}" if default_mode == :by
|
|
86
|
+
|
|
87
87
|
target.defaults[:disallow_cursor] = true
|
|
88
88
|
end
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
def default(spec)
|
|
92
|
-
unless spec.is_a?(Hash) && spec.keys.size == 1 && [
|
|
92
|
+
unless spec.is_a?(Hash) && spec.keys.size == 1 && %i[by page].include?(spec.keys.first)
|
|
93
93
|
raise "'default' syntax for pagination takes exactly one key specification. Either by: <:fieldname> or page: <num>" \
|
|
94
94
|
"#{spec} is invalid"
|
|
95
95
|
end
|
|
96
96
|
mode, value = spec.first
|
|
97
97
|
def_mode = case mode
|
|
98
98
|
when :by
|
|
99
|
-
if target.fields_allowed && !target.fields_allowed&.include?(value)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if target.defaults[:disallow_cursor]
|
|
103
|
-
raise "Cannot define a default pagination that is cursor based, if cursor-based pagination is disallowed."
|
|
104
|
-
end
|
|
99
|
+
raise "Error setting default pagination. Field #{value} is not amongst the allowed fields." if target.fields_allowed && !target.fields_allowed&.include?(value)
|
|
100
|
+
raise 'Cannot define a default pagination that is cursor based, if cursor-based pagination is disallowed.' if target.defaults[:disallow_cursor]
|
|
101
|
+
|
|
105
102
|
{ by: value }
|
|
106
103
|
when :page
|
|
107
|
-
unless value.is_a?(Integer)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if target.defaults[:disallow_paging]
|
|
111
|
-
raise "Cannot define a default pagination that is page-based, if page-based pagination is disallowed."
|
|
112
|
-
end
|
|
104
|
+
raise "Error setting default pagination. Initial page should be a integer (but got #{value})" unless value.is_a?(Integer)
|
|
105
|
+
raise 'Cannot define a default pagination that is page-based, if page-based pagination is disallowed.' if target.defaults[:disallow_paging]
|
|
106
|
+
|
|
113
107
|
{ page: value }
|
|
114
108
|
end
|
|
115
109
|
target.defaults[:default_mode] = def_mode
|
|
@@ -141,9 +135,8 @@ module Praxis
|
|
|
141
135
|
|
|
142
136
|
def self.paging_default_mode(newval = nil)
|
|
143
137
|
if newval
|
|
144
|
-
unless newval.respond_to?(:keys) && newval.keys.size == 1 && [
|
|
145
|
-
|
|
146
|
-
end
|
|
138
|
+
raise 'Error setting paging_default_mode, value must be a hash with :by or :page keys' unless newval.respond_to?(:keys) && newval.keys.size == 1 && %i[by page].include?(newval.keys.first)
|
|
139
|
+
|
|
147
140
|
@paging_default_mode = newval
|
|
148
141
|
end
|
|
149
142
|
@paging_default_mode
|
|
@@ -152,14 +145,13 @@ module Praxis
|
|
|
152
145
|
# Abstract class, which needs to be used by subclassing it through the .for method, to link it to a particular
|
|
153
146
|
# MediaType, so that the field name checking and value coercion can be performed
|
|
154
147
|
class << self
|
|
155
|
-
attr_reader :media_type
|
|
156
|
-
attr_reader :defaults
|
|
148
|
+
attr_reader :media_type, :defaults
|
|
157
149
|
attr_accessor :fields_allowed
|
|
158
150
|
|
|
159
151
|
def for(media_type, **_opts)
|
|
160
152
|
unless media_type < Praxis::MediaType
|
|
161
153
|
raise ArgumentError, "Invalid type: #{media_type.name} for Paginator. " \
|
|
162
|
-
|
|
154
|
+
'Must be a subclass of MediaType'
|
|
163
155
|
end
|
|
164
156
|
|
|
165
157
|
::Class.new(self) do
|
|
@@ -206,11 +198,7 @@ module Praxis
|
|
|
206
198
|
self
|
|
207
199
|
end
|
|
208
200
|
|
|
209
|
-
attr_reader :by
|
|
210
|
-
attr_reader :from
|
|
211
|
-
attr_reader :items
|
|
212
|
-
attr_reader :page
|
|
213
|
-
attr_reader :total_count
|
|
201
|
+
attr_reader :by, :from, :items, :page, :total_count
|
|
214
202
|
|
|
215
203
|
def self.example(_context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
|
|
216
204
|
fields = if media_type
|
|
@@ -224,10 +212,10 @@ module Praxis
|
|
|
224
212
|
from = media_type.attributes[by].example(parent: mt_example).to_s
|
|
225
213
|
# Make sure to encode the value of the from, as it can contain commas and such
|
|
226
214
|
# Only add the from parameter if it's not empty (an empty from is not allowed and it's gonna blow up in load)
|
|
227
|
-
optional_from_component =
|
|
215
|
+
optional_from_component = from && !from.empty? ? ",from=#{CGI.escape(from)}" : ''
|
|
228
216
|
"by=#{by}#{optional_from_component},items=#{defaults[:page_size]}"
|
|
229
217
|
else
|
|
230
|
-
|
|
218
|
+
'by=id,from=20,items=100'
|
|
231
219
|
end
|
|
232
220
|
load(fields)
|
|
233
221
|
end
|
|
@@ -237,9 +225,10 @@ module Praxis
|
|
|
237
225
|
instance.validate(context)
|
|
238
226
|
end
|
|
239
227
|
|
|
240
|
-
CLAUSE_REGEX = /(?<type>[^=]+)=(?<value>.+)
|
|
228
|
+
CLAUSE_REGEX = /(?<type>[^=]+)=(?<value>.+)$/.freeze
|
|
241
229
|
def self.load(paginator, _context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
|
|
242
230
|
return paginator if paginator.is_a?(native_type) || paginator.nil?
|
|
231
|
+
|
|
243
232
|
parsed = {}
|
|
244
233
|
unless paginator.nil?
|
|
245
234
|
parsed = paginator.split(',').each_with_object({}) do |paginator_string, hash|
|
|
@@ -305,7 +294,7 @@ module Praxis
|
|
|
305
294
|
if media_type&.attributes
|
|
306
295
|
attrs = media_type&.attributes || {}
|
|
307
296
|
attribute = attrs[name.to_sym]
|
|
308
|
-
attribute
|
|
297
|
+
attribute&.type&.load(value)
|
|
309
298
|
else
|
|
310
299
|
value
|
|
311
300
|
end
|
|
@@ -320,28 +309,20 @@ module Praxis
|
|
|
320
309
|
@total_count = parsed[:total_count]
|
|
321
310
|
end
|
|
322
311
|
|
|
323
|
-
def validate(_context = Attributor::DEFAULT_ROOT_CONTEXT)
|
|
312
|
+
def validate(_context = Attributor::DEFAULT_ROOT_CONTEXT)
|
|
324
313
|
errors = []
|
|
325
314
|
|
|
326
315
|
if page
|
|
327
|
-
if self.class.defaults[:disallow_paging]
|
|
328
|
-
errors << "Page-based pagination is disallowed (i.e., using 'page=' parameter)"
|
|
329
|
-
end
|
|
316
|
+
errors << "Page-based pagination is disallowed (i.e., using 'page=' parameter)" if self.class.defaults[:disallow_paging]
|
|
330
317
|
elsif self.class.defaults[:disallow_cursor]
|
|
331
318
|
errors << "Cursor-based pagination is disallowed (i.e., using 'by=' or 'from=' parameter)"
|
|
332
319
|
end
|
|
333
320
|
|
|
334
|
-
if page && page <= 0
|
|
335
|
-
errors << "Page parameter cannot be zero or negative! (got: #{parsed.page})"
|
|
336
|
-
end
|
|
321
|
+
errors << "Page parameter cannot be zero or negative! (got: #{parsed.page})" if page && page <= 0
|
|
337
322
|
|
|
338
|
-
if items && (items <= 0 || (
|
|
339
|
-
errors << "Value of 'items' is invalid (got: #{items}). It must be positive, and smaller than the maximum amount of items per request (set to #{self.class.defaults[:max_items]})"
|
|
340
|
-
end
|
|
323
|
+
errors << "Value of 'items' is invalid (got: #{items}). It must be positive, and smaller than the maximum amount of items per request (set to #{self.class.defaults[:max_items]})" if items && (items <= 0 || (self.class.defaults[:max_items] && items > self.class.defaults[:max_items]))
|
|
341
324
|
|
|
342
|
-
if page && (by || from)
|
|
343
|
-
errors << "Cannot specify the field to use and its start value to paginate from when using a fix pager (i.e., `by` and/or `from` params are not compabible with `page`)"
|
|
344
|
-
end
|
|
325
|
+
errors << 'Cannot specify the field to use and its start value to paginate from when using a fix pager (i.e., `by` and/or `from` params are not compabible with `page`)' if page && (by || from)
|
|
345
326
|
|
|
346
327
|
if by && self.class.fields_allowed && !self.class.fields_allowed.include?(by.to_sym)
|
|
347
328
|
errors << if self.class.media_type.attributes.key?(by.to_sym)
|
|
@@ -364,7 +345,7 @@ module Praxis
|
|
|
364
345
|
s
|
|
365
346
|
end
|
|
366
347
|
str += ",items=#{items}" if @items
|
|
367
|
-
str +=
|
|
348
|
+
str += ',total_count=true' if @total_count
|
|
368
349
|
str
|
|
369
350
|
end
|
|
370
351
|
end
|
|
@@ -1,20 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative 'pagination_handler'
|
|
2
4
|
|
|
3
5
|
module Praxis
|
|
4
6
|
module Extensions
|
|
5
7
|
module Pagination
|
|
6
8
|
class SequelPaginationHandler < PaginationHandler
|
|
7
|
-
|
|
8
9
|
def self.where_lt(query, attr, value)
|
|
9
10
|
query.where("#{attr} < ?", value)
|
|
10
11
|
end
|
|
11
|
-
|
|
12
|
+
|
|
12
13
|
def self.where_gt(query, attr, value)
|
|
13
14
|
query.where("#{attr} > ?", value)
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def self.order(query, order)
|
|
17
18
|
return query unless order
|
|
19
|
+
|
|
18
20
|
order_clause = order.map do |spec_hash|
|
|
19
21
|
direction, name = spec_hash.first
|
|
20
22
|
case direction.to_sym
|
|
@@ -24,21 +26,20 @@ module Praxis
|
|
|
24
26
|
Sequel.asc(name.to_sym)
|
|
25
27
|
end
|
|
26
28
|
end
|
|
27
|
-
query
|
|
28
|
-
query
|
|
29
|
+
query.order(*order_clause)
|
|
29
30
|
end
|
|
30
|
-
|
|
31
|
+
|
|
31
32
|
def self.count(query)
|
|
32
33
|
query.count
|
|
33
34
|
end
|
|
34
|
-
|
|
35
|
+
|
|
35
36
|
def self.offset(query, offset)
|
|
36
37
|
query.offset(offset)
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
def self.limit(query, limit)
|
|
40
41
|
query.limit(limit)
|
|
41
|
-
end
|
|
42
|
+
end
|
|
42
43
|
end
|
|
43
44
|
end
|
|
44
45
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
begin
|
|
2
4
|
require 'link_header'
|
|
3
5
|
rescue LoadError
|
|
@@ -43,17 +45,13 @@ module Praxis
|
|
|
43
45
|
|
|
44
46
|
included do
|
|
45
47
|
after :action do |controller, _callee|
|
|
46
|
-
if controller.response.status < 300
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
current_query_params: controller.request.query
|
|
54
|
-
)
|
|
55
|
-
controller.response.headers.merge! headers
|
|
56
|
-
end
|
|
48
|
+
if controller.response.status < 300 && controller._pagination.paginator
|
|
49
|
+
headers = controller.build_pagination_headers(
|
|
50
|
+
pagination: controller._pagination,
|
|
51
|
+
current_url: controller.request.path,
|
|
52
|
+
current_query_params: controller.request.query
|
|
53
|
+
)
|
|
54
|
+
controller.response.headers.merge! headers
|
|
57
55
|
end
|
|
58
56
|
end
|
|
59
57
|
end
|
|
@@ -75,9 +73,7 @@ module Praxis
|
|
|
75
73
|
links = if pagination.paginator.by
|
|
76
74
|
# We're assuming that the last element has a "symbol/string" field with the same name of the "by" pagination.
|
|
77
75
|
last_element = response.body.last
|
|
78
|
-
if last_element
|
|
79
|
-
last_value = last_element[pagination.paginator.by.to_sym] || last_element[pagination.paginator.by]
|
|
80
|
-
end
|
|
76
|
+
last_value = last_element[pagination.paginator.by.to_sym] || last_element[pagination.paginator.by] if last_element
|
|
81
77
|
HeaderGenerator.build_cursor_headers(
|
|
82
78
|
paginator: pagination.paginator,
|
|
83
79
|
last_value: last_value,
|
|
@@ -97,7 +93,6 @@ module Praxis
|
|
|
97
93
|
total_count: pagination.total_count
|
|
98
94
|
)
|
|
99
95
|
end
|
|
100
|
-
|
|
101
96
|
end
|
|
102
97
|
end
|
|
103
98
|
end
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# Make Praxis' request derive from ActionDispatch
|
|
2
4
|
if defined? Praxis::Request
|
|
3
5
|
puts "IT seems that we're trying to redefine Praxis' request parent too late."
|
|
4
|
-
puts
|
|
6
|
+
puts '-> try to include the Rails compat pieces earlier in the bootstrap process (before Praxis::Request is requried)'
|
|
5
7
|
exit(-1)
|
|
6
8
|
end
|
|
7
9
|
|
|
@@ -14,6 +16,3 @@ begin
|
|
|
14
16
|
end
|
|
15
17
|
require 'praxis/request'
|
|
16
18
|
end
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
@@ -1,32 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Praxis
|
|
2
4
|
module Extensions
|
|
3
|
-
|
|
4
5
|
module Rendering
|
|
5
6
|
extend ActiveSupport::Concern
|
|
6
7
|
include FieldExpansion
|
|
7
8
|
|
|
8
9
|
def render(object, include_nil: false)
|
|
9
|
-
loaded =
|
|
10
|
+
loaded = media_type.load(object)
|
|
10
11
|
renderer = Praxis::Renderer.new(include_nil: include_nil)
|
|
11
|
-
renderer.render(loaded,
|
|
12
|
+
renderer.render(loaded, expanded_fields)
|
|
12
13
|
rescue Attributor::DumpError
|
|
13
|
-
if
|
|
14
|
-
warn "Detected the rendering of an object of type #{
|
|
15
|
-
|
|
14
|
+
if media_type.domain_model == Object
|
|
15
|
+
warn "Detected the rendering of an object of type #{media_type} without having a domain object model set.\n" \
|
|
16
|
+
'Did you forget to define it?'
|
|
16
17
|
end
|
|
17
18
|
raise
|
|
18
19
|
end
|
|
19
20
|
|
|
20
|
-
def display(object, include_nil: false, encoder:
|
|
21
|
-
identifier = Praxis::MediaTypeIdentifier.load(
|
|
21
|
+
def display(object, include_nil: false, encoder: default_encoder)
|
|
22
|
+
identifier = Praxis::MediaTypeIdentifier.load(media_type.identifier)
|
|
22
23
|
identifier += encoder unless encoder.blank?
|
|
23
24
|
response.headers['Content-Type'] = identifier.to_s
|
|
24
25
|
response.body = render(object, include_nil: include_nil)
|
|
25
26
|
response
|
|
26
27
|
rescue Praxis::Renderer::CircularRenderingError => e
|
|
27
|
-
|
|
28
|
-
summary:
|
|
29
|
-
|
|
28
|
+
Praxis::Application.instance.validation_handler.handle!(
|
|
29
|
+
summary: 'Circular Rendering Error when rendering response. ' \
|
|
30
|
+
'Please especify a view to narrow the dependent fields, or narrow your field set.',
|
|
30
31
|
exception: e,
|
|
31
32
|
request: request,
|
|
32
33
|
stage: :action,
|
|
@@ -37,7 +38,6 @@ module Praxis
|
|
|
37
38
|
def default_encoder
|
|
38
39
|
''
|
|
39
40
|
end
|
|
40
|
-
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
end
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Praxis
|
|
3
4
|
class FieldExpander
|
|
4
5
|
def self.expand(object, fields = true)
|
|
5
6
|
new.expand(object, fields)
|
|
6
7
|
end
|
|
7
8
|
|
|
8
|
-
attr_reader :stack
|
|
9
|
-
attr_reader :history
|
|
9
|
+
attr_reader :stack, :history
|
|
10
10
|
|
|
11
11
|
def initialize
|
|
12
12
|
@stack = Hash.new do |hash, key|
|
|
@@ -20,6 +20,7 @@ module Praxis
|
|
|
20
20
|
def expand(object, fields = true)
|
|
21
21
|
if stack[object].include? fields
|
|
22
22
|
return history[object][fields] if history[object].include? fields
|
|
23
|
+
|
|
23
24
|
# We should probably never get here, since we should have a record
|
|
24
25
|
# of the history of an expansion if we're trying to redo it,
|
|
25
26
|
# but we should also be conservative and raise here just in case.
|
|
@@ -61,11 +62,9 @@ module Praxis
|
|
|
61
62
|
|
|
62
63
|
def expand_type(object, fields = true)
|
|
63
64
|
unless object.respond_to?(:attributes)
|
|
64
|
-
if object.respond_to?(:member_attribute)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return true
|
|
68
|
-
end
|
|
65
|
+
return expand_type(object.member_attribute.type, fields) if object.respond_to?(:member_attribute)
|
|
66
|
+
|
|
67
|
+
return true
|
|
69
68
|
end
|
|
70
69
|
|
|
71
70
|
# just include the full thing if it has no attributes
|
|
@@ -80,11 +79,11 @@ module Praxis
|
|
|
80
79
|
result = expand_fields(object.attributes, fields) do |dumpable, sub_fields|
|
|
81
80
|
expand(dumpable.type, sub_fields)
|
|
82
81
|
end
|
|
83
|
-
unless fields == true
|
|
82
|
+
unless fields == true
|
|
84
83
|
non_matching = fields.keys - object.attributes.keys
|
|
85
84
|
raise "FieldExpansion error: attribute(s) #{non_matching} do not exist in #{object}" unless non_matching.empty?
|
|
86
85
|
end
|
|
87
86
|
history[object][fields].merge!(result)
|
|
88
87
|
end
|
|
89
88
|
end
|
|
90
|
-
end
|
|
89
|
+
end
|
data/lib/praxis/file_group.rb
CHANGED
|
@@ -1,26 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
module Praxis
|
|
3
4
|
class FileGroup
|
|
4
|
-
|
|
5
5
|
attr_reader :groups, :base
|
|
6
6
|
|
|
7
7
|
def initialize(base, &block)
|
|
8
8
|
if base.nil?
|
|
9
|
-
raise ArgumentError,
|
|
10
|
-
|
|
9
|
+
raise ArgumentError, 'base must not be nil.' \
|
|
10
|
+
'Are you missing a call Praxis::Application.instance.setup?'
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
@groups = Hash.new
|
|
13
|
+
@groups = {}
|
|
15
14
|
@base = Pathname.new(base)
|
|
16
15
|
|
|
17
|
-
if block_given?
|
|
18
|
-
self.instance_eval(&block)
|
|
19
|
-
end
|
|
16
|
+
instance_eval(&block) if block_given?
|
|
20
17
|
end
|
|
21
18
|
|
|
22
19
|
def layout(&block)
|
|
23
|
-
|
|
20
|
+
instance_eval(&block)
|
|
24
21
|
end
|
|
25
22
|
|
|
26
23
|
def map(name, pattern, &block)
|
|
@@ -30,7 +27,7 @@ module Praxis
|
|
|
30
27
|
@groups[name] = FileGroup.new(base + pattern, &block)
|
|
31
28
|
else
|
|
32
29
|
@groups[name] ||= []
|
|
33
|
-
files = Pathname.glob(base+pattern).select
|
|
30
|
+
files = Pathname.glob(base + pattern).select(&:file?)
|
|
34
31
|
files.sort_by! { |file| [file.to_s.split('/').size, file.to_s] }
|
|
35
32
|
files.each { |file| @groups[name] << file }
|
|
36
33
|
end
|
|
@@ -39,6 +36,5 @@ module Praxis
|
|
|
39
36
|
def [](*names)
|
|
40
37
|
names.inject(@groups) { |group, name| group[name] || [] }
|
|
41
38
|
end
|
|
42
|
-
|
|
43
39
|
end
|
|
44
40
|
end
|
data/lib/praxis/finalizable.rb
CHANGED
data/lib/praxis/handlers/json.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Praxis
|
|
2
4
|
module Handlers
|
|
3
5
|
class JSON
|
|
@@ -9,7 +11,7 @@ module Praxis
|
|
|
9
11
|
rescue LoadError
|
|
10
12
|
# Should never happen since JSON is a default gem; might as well be cautious!
|
|
11
13
|
raise Praxis::Exceptions::InvalidConfiguration,
|
|
12
|
-
|
|
14
|
+
'JSON handler depends on json ~> 1.0; please add it to your Gemfile'
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
# Parse a JSON document into structured data.
|
|
@@ -18,7 +20,8 @@ module Praxis
|
|
|
18
20
|
# @return [Hash,Array] the structured-data representation of the document
|
|
19
21
|
def parse(document)
|
|
20
22
|
# Try to be nice and accept an empty string as an empty payload (seems nice to do for dumb http clients)
|
|
21
|
-
return nil if
|
|
23
|
+
return nil if document.nil? || document == ''
|
|
24
|
+
|
|
22
25
|
::JSON.parse(document)
|
|
23
26
|
end
|
|
24
27
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# This is an example of a handler that can load and generate www-url-encoded payloads.
|
|
2
|
-
# Note that if you use your API to pass nil values for attributes as a way to unset their
|
|
3
|
-
# values, this handler will not work (as there isn't necessarily a defined "null" value in
|
|
4
|
+
# Note that if you use your API to pass nil values for attributes as a way to unset their
|
|
5
|
+
# values, this handler will not work (as there isn't necessarily a defined "null" value in
|
|
4
6
|
# this encoding (although you can probably define how to encode/decode it and use it as such)
|
|
5
7
|
# Use at your own risk.
|
|
6
8
|
module Praxis
|
|
@@ -19,8 +21,9 @@ module Praxis
|
|
|
19
21
|
# is not very useful for a response body.
|
|
20
22
|
def generate(structured_data)
|
|
21
23
|
return nil if structured_data.nil?
|
|
24
|
+
|
|
22
25
|
URI.encode_www_form(structured_data)
|
|
23
26
|
end
|
|
24
27
|
end
|
|
25
28
|
end
|
|
26
|
-
end
|
|
29
|
+
end
|