praxis 2.0.pre.8 → 2.0.pre.13
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/.gitignore +2 -0
- data/.ruby-version +1 -1
- data/.travis.yml +1 -3
- data/CHANGELOG.md +33 -0
- data/TODO.md +1 -4
- data/bin/praxis +67 -12
- data/lib/praxis.rb +10 -3
- data/lib/praxis/action_definition.rb +15 -13
- data/lib/praxis/action_definition/headers_dsl_compiler.rb +0 -7
- data/lib/praxis/api_general_info.rb +1 -1
- data/lib/praxis/application.rb +6 -2
- data/lib/praxis/blueprint.rb +357 -0
- data/lib/praxis/bootloader.rb +9 -3
- data/lib/praxis/bootloader_stages/environment.rb +16 -13
- data/lib/praxis/collection.rb +1 -11
- data/lib/praxis/config_hash.rb +44 -0
- data/lib/praxis/docs/{openapi → open_api}/info_object.rb +18 -10
- data/lib/praxis/docs/{openapi → open_api}/media_type_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/operation_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/parameter_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/paths_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/request_body_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/response_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/responses_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/schema_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/server_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/tag_object.rb +0 -0
- data/lib/praxis/docs/open_api_generator.rb +91 -6
- data/lib/praxis/endpoint_definition.rb +273 -0
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +182 -58
- data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +3 -2
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +47 -56
- data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +153 -0
- data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +20 -8
- data/lib/praxis/extensions/field_expansion.rb +3 -36
- data/lib/praxis/extensions/pagination.rb +5 -32
- data/lib/praxis/extensions/pagination/ordering_params.rb +1 -1
- data/lib/praxis/extensions/pagination/pagination_params.rb +6 -4
- data/lib/praxis/field_expander.rb +90 -0
- data/lib/praxis/finalizable.rb +34 -0
- data/lib/praxis/mapper/active_model_compat.rb +4 -0
- data/lib/praxis/mapper/resource.rb +18 -2
- data/lib/praxis/mapper/selector_generator.rb +2 -1
- data/lib/praxis/mapper/sequel_compat.rb +7 -0
- data/lib/praxis/media_type.rb +3 -68
- data/lib/praxis/plugin_concern.rb +1 -1
- data/lib/praxis/plugins/mapper_plugin.rb +24 -15
- data/lib/praxis/plugins/pagination_plugin.rb +34 -4
- data/lib/praxis/renderer.rb +88 -0
- data/lib/praxis/request.rb +1 -1
- data/lib/praxis/resource_definition.rb +2 -311
- data/lib/praxis/response_definition.rb +2 -10
- data/lib/praxis/response_template.rb +3 -3
- data/lib/praxis/router.rb +2 -2
- data/lib/praxis/routing_config.rb +1 -1
- data/lib/praxis/tasks/api_docs.rb +17 -64
- data/lib/praxis/tasks/routes.rb +1 -1
- data/lib/praxis/types/media_type_common.rb +1 -11
- data/lib/praxis/version.rb +1 -1
- data/praxis.gemspec +0 -1
- data/spec/functional_spec.rb +5 -9
- data/spec/praxis/action_definition_spec.rb +12 -20
- data/spec/praxis/blueprint_spec.rb +373 -0
- data/spec/praxis/bootloader_spec.rb +10 -2
- data/spec/praxis/collection_spec.rb +0 -13
- data/spec/praxis/config_hash_spec.rb +64 -0
- data/spec/praxis/{resource_definition_spec.rb → endpoint_definition_spec.rb} +37 -64
- data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +249 -168
- data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +25 -6
- data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +190 -8
- data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +140 -0
- data/spec/praxis/extensions/field_expansion_spec.rb +5 -24
- data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +1 -1
- data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +1 -1
- data/spec/praxis/extensions/support/spec_resources_active_model.rb +1 -1
- data/spec/praxis/field_expander_spec.rb +149 -0
- data/spec/praxis/mapper/selector_generator_spec.rb +1 -1
- data/spec/praxis/media_type_identifier_spec.rb +5 -4
- data/spec/praxis/media_type_spec.rb +4 -93
- data/spec/praxis/renderer_spec.rb +188 -0
- data/spec/praxis/response_definition_spec.rb +0 -31
- data/spec/praxis/response_spec.rb +1 -1
- data/spec/praxis/router_spec.rb +8 -8
- data/spec/praxis/routing_config_spec.rb +3 -3
- data/spec/spec_app/app/controllers/instances.rb +13 -7
- data/spec/spec_app/design/media_types/instance.rb +1 -19
- data/spec/spec_app/design/media_types/volume.rb +1 -1
- data/spec/spec_app/design/media_types/volume_snapshot.rb +2 -14
- data/spec/spec_app/design/resources/instances.rb +5 -8
- data/spec/spec_app/design/resources/volume_snapshots.rb +1 -1
- data/spec/spec_app/design/resources/volumes.rb +1 -1
- data/spec/support/spec_authorization_plugin.rb +1 -1
- data/spec/support/spec_blueprints.rb +72 -0
- data/spec/support/{spec_resource_definitions.rb → spec_endpoint_definitions.rb} +2 -2
- data/spec/support/spec_media_types.rb +6 -26
- data/tasks/thor/app.rb +8 -34
- data/tasks/thor/example.rb +51 -285
- data/tasks/thor/model.rb +40 -0
- data/tasks/thor/scaffold.rb +117 -0
- data/tasks/thor/templates/generator/empty_app/.gitignore +0 -1
- data/tasks/thor/templates/generator/empty_app/Gemfile +7 -23
- data/tasks/thor/templates/generator/empty_app/README.md +1 -1
- data/tasks/thor/templates/generator/empty_app/Rakefile +4 -13
- data/tasks/thor/templates/generator/empty_app/{design/response_templates → app/v1/resources}/.empty_directory +0 -0
- data/tasks/thor/templates/generator/empty_app/{design/response_templates → app/v1/resources}/.gitkeep +0 -0
- data/tasks/thor/templates/generator/empty_app/config/environment.rb +26 -17
- data/tasks/thor/templates/generator/empty_app/{design/v1/resources → config/initializers}/.empty_directory +0 -0
- data/tasks/thor/templates/generator/empty_app/{design/v1/resources → config/initializers}/.gitkeep +0 -0
- data/tasks/thor/templates/generator/empty_app/design/v1/endpoints/.empty_directory +0 -0
- data/tasks/thor/templates/generator/empty_app/design/v1/endpoints/.gitkeep +0 -0
- data/tasks/thor/templates/generator/empty_app/docs/.empty_directory +0 -0
- data/tasks/thor/templates/generator/empty_app/docs/.gitkeep +0 -0
- data/tasks/thor/templates/generator/empty_app/spec/spec_helper.rb +14 -9
- data/tasks/thor/templates/generator/example_app/.gitignore +1 -0
- data/tasks/thor/templates/generator/example_app/Gemfile +19 -0
- data/tasks/thor/templates/generator/example_app/Rakefile +61 -0
- data/tasks/thor/templates/generator/example_app/app/models/user.rb +6 -0
- data/tasks/thor/templates/generator/example_app/app/v1/concerns/controller_base.rb +24 -0
- data/tasks/thor/templates/generator/example_app/app/v1/controllers/users.rb +17 -0
- data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +11 -0
- data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +25 -0
- data/tasks/thor/templates/generator/example_app/config.ru +30 -0
- data/tasks/thor/templates/generator/example_app/config/environment.rb +41 -0
- data/tasks/thor/templates/generator/example_app/db/migrate/20201010101010_create_users_table.rb +12 -0
- data/tasks/thor/templates/generator/example_app/db/seeds.rb +6 -0
- data/tasks/thor/templates/generator/example_app/design/api.rb +18 -0
- data/tasks/thor/templates/generator/example_app/design/v1/endpoints/users.rb +37 -0
- data/tasks/thor/templates/generator/example_app/design/v1/media_types/user.rb +21 -0
- data/tasks/thor/templates/generator/example_app/spec/helpers/database_helper.rb +20 -0
- data/tasks/thor/templates/generator/example_app/spec/spec_helper.rb +42 -0
- data/tasks/thor/templates/generator/example_app/spec/v1/controllers/users_spec.rb +37 -0
- data/tasks/thor/templates/generator/scaffold/design/endpoints/collection.rb +98 -0
- data/tasks/thor/templates/generator/scaffold/design/media_types/item.rb +18 -0
- data/tasks/thor/templates/generator/scaffold/implementation/controllers/collection.rb +77 -0
- data/tasks/thor/templates/generator/scaffold/implementation/resources/base.rb +11 -0
- data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +45 -0
- data/tasks/thor/templates/generator/scaffold/models/active_record.rb +6 -0
- data/tasks/thor/templates/generator/scaffold/models/sequel.rb +6 -0
- metadata +64 -136
- data/lib/api_browser/.bowerrc +0 -3
- data/lib/api_browser/.editorconfig +0 -21
- data/lib/api_browser/Gruntfile.js +0 -581
- data/lib/api_browser/app/index.html +0 -59
- data/lib/api_browser/app/js/app.js +0 -48
- data/lib/api_browser/app/js/controllers/action.js +0 -47
- data/lib/api_browser/app/js/controllers/controller.js +0 -10
- data/lib/api_browser/app/js/controllers/menu.js +0 -93
- data/lib/api_browser/app/js/controllers/trait.js +0 -10
- data/lib/api_browser/app/js/controllers/type.js +0 -24
- data/lib/api_browser/app/js/directives/attribute_description.js +0 -56
- data/lib/api_browser/app/js/directives/attribute_table.js +0 -28
- data/lib/api_browser/app/js/directives/conditional_requirements.js +0 -13
- data/lib/api_browser/app/js/directives/fixed_if_fits.js +0 -38
- data/lib/api_browser/app/js/directives/highlight.js +0 -14
- data/lib/api_browser/app/js/directives/menu_item.js +0 -59
- data/lib/api_browser/app/js/directives/no_container.js +0 -8
- data/lib/api_browser/app/js/directives/readable_list.js +0 -87
- data/lib/api_browser/app/js/directives/request_examples.js +0 -31
- data/lib/api_browser/app/js/directives/type_placeholder.js +0 -30
- data/lib/api_browser/app/js/directives/url.js +0 -15
- data/lib/api_browser/app/js/factories/Configuration.js +0 -12
- data/lib/api_browser/app/js/factories/Documentation.js +0 -61
- data/lib/api_browser/app/js/factories/Example.js +0 -51
- data/lib/api_browser/app/js/factories/PageInfo.js +0 -9
- data/lib/api_browser/app/js/factories/normalize_attributes.js +0 -20
- data/lib/api_browser/app/js/factories/prepare_template.js +0 -15
- data/lib/api_browser/app/js/factories/template_for.js +0 -128
- data/lib/api_browser/app/js/filters/attribute_name.js +0 -10
- data/lib/api_browser/app/js/filters/friendly_json.js +0 -5
- data/lib/api_browser/app/js/filters/has_requirement.js +0 -14
- data/lib/api_browser/app/js/filters/header_info.js +0 -9
- data/lib/api_browser/app/js/filters/is_empty.js +0 -8
- data/lib/api_browser/app/js/filters/markdown.js +0 -6
- data/lib/api_browser/app/js/filters/resource_name.js +0 -5
- data/lib/api_browser/app/js/filters/tag_requirement.js +0 -13
- data/lib/api_browser/app/sass/modules/_body.scss +0 -40
- data/lib/api_browser/app/sass/modules/_cloke.scss +0 -8
- data/lib/api_browser/app/sass/modules/_header.scss +0 -10
- data/lib/api_browser/app/sass/modules/_nav.scss +0 -7
- data/lib/api_browser/app/sass/modules/_sidebar.scss +0 -134
- data/lib/api_browser/app/sass/modules/_switch.scss +0 -55
- data/lib/api_browser/app/sass/modules/_table.scss +0 -13
- data/lib/api_browser/app/sass/praxis.scss +0 -70
- data/lib/api_browser/app/sass/variables/_bootstrap-variables.scss +0 -774
- data/lib/api_browser/app/views/action.html +0 -97
- data/lib/api_browser/app/views/builtin/field-selector.html +0 -24
- data/lib/api_browser/app/views/controller.html +0 -55
- data/lib/api_browser/app/views/directives/attribute_description.html +0 -2
- data/lib/api_browser/app/views/directives/attribute_description/default.html +0 -2
- data/lib/api_browser/app/views/directives/attribute_description/example.html +0 -13
- data/lib/api_browser/app/views/directives/attribute_description/headers.html +0 -8
- data/lib/api_browser/app/views/directives/attribute_description/member_options.html +0 -4
- data/lib/api_browser/app/views/directives/attribute_description/values.html +0 -14
- data/lib/api_browser/app/views/directives/attribute_table.html +0 -17
- data/lib/api_browser/app/views/directives/menu_item.html +0 -8
- data/lib/api_browser/app/views/directives/url.html +0 -3
- data/lib/api_browser/app/views/examples/general.html +0 -26
- data/lib/api_browser/app/views/home.html +0 -5
- data/lib/api_browser/app/views/layout.html +0 -8
- data/lib/api_browser/app/views/menu.html +0 -42
- data/lib/api_browser/app/views/navbar.html +0 -9
- data/lib/api_browser/app/views/trait.html +0 -13
- data/lib/api_browser/app/views/type.html +0 -6
- data/lib/api_browser/app/views/type/details.html +0 -33
- data/lib/api_browser/app/views/types/embedded/array.html +0 -2
- data/lib/api_browser/app/views/types/embedded/default.html +0 -12
- data/lib/api_browser/app/views/types/embedded/field-selector.html +0 -13
- data/lib/api_browser/app/views/types/embedded/links.html +0 -11
- data/lib/api_browser/app/views/types/embedded/requirements.html +0 -6
- data/lib/api_browser/app/views/types/embedded/single_req.html +0 -9
- data/lib/api_browser/app/views/types/embedded/struct.html +0 -14
- data/lib/api_browser/app/views/types/label/link.html +0 -1
- data/lib/api_browser/app/views/types/label/primitive.html +0 -1
- data/lib/api_browser/app/views/types/label/primitive_collection.html +0 -1
- data/lib/api_browser/app/views/types/label/type.html +0 -1
- data/lib/api_browser/app/views/types/label/type_collection.html +0 -1
- data/lib/api_browser/app/views/types/main/array.html +0 -22
- data/lib/api_browser/app/views/types/main/default.html +0 -23
- data/lib/api_browser/app/views/types/main/hash.html +0 -23
- data/lib/api_browser/app/views/types/standalone/array.html +0 -3
- data/lib/api_browser/app/views/types/standalone/default.html +0 -18
- data/lib/api_browser/app/views/types/standalone/struct.html +0 -2
- data/lib/api_browser/bower_template.json +0 -41
- data/lib/api_browser/package-lock.json +0 -7110
- data/lib/api_browser/package.json +0 -43
- data/lib/praxis/docs/generator.rb +0 -243
- data/lib/praxis/docs/link_builder.rb +0 -30
- data/lib/praxis/links.rb +0 -135
- data/lib/praxis/types/multipart.rb +0 -109
- data/spec/api_browser/directives/type_placeholder_spec.js +0 -134
- data/spec/api_browser/factories/configuration_spec.js +0 -32
- data/spec/api_browser/factories/documentation_spec.js +0 -100
- data/spec/api_browser/factories/normalize_attributes_spec.js +0 -92
- data/spec/api_browser/factories/template_for_spec.js +0 -67
- data/spec/api_browser/filters/attribute_name_spec.js +0 -23
- data/spec/praxis/types/multipart_spec.rb +0 -112
- data/tasks/thor/templates/generator/empty_app/.rspec +0 -1
- data/tasks/thor/templates/generator/empty_app/Guardfile +0 -3
- data/tasks/thor/templates/generator/empty_app/config/rainbows.rb +0 -57
- data/tasks/thor/templates/generator/empty_app/docs/app.js +0 -1
- data/tasks/thor/templates/generator/empty_app/docs/styles.scss +0 -3
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
require 'parslet'
|
|
2
|
+
|
|
3
|
+
module Praxis
|
|
4
|
+
module Extensions
|
|
5
|
+
module AttributeFiltering
|
|
6
|
+
class FilteringParams
|
|
7
|
+
class Condition
|
|
8
|
+
attr_reader :name, :op, :values
|
|
9
|
+
attr_accessor :parent_group
|
|
10
|
+
|
|
11
|
+
# For operands with a single or no values: Incoming data is a hash with name and op
|
|
12
|
+
# For Operands with multiple values: Incoming data is an array of hashes
|
|
13
|
+
# First hash has the spec (i.e., name and op)
|
|
14
|
+
# The rest of the hashes contain a value each (a hash with value: X each).
|
|
15
|
+
# Example: [{:name=>"multi"@0, :op=>"="@5}, {:value=>"1"@6}, {:value=>"2"@8}]
|
|
16
|
+
def initialize(triad:, parent_group:)
|
|
17
|
+
@parent_group = parent_group
|
|
18
|
+
|
|
19
|
+
if triad.is_a? Array # several values coming in
|
|
20
|
+
spec, *values = triad
|
|
21
|
+
@name = spec[:name].to_sym
|
|
22
|
+
@op = spec[:op].to_s
|
|
23
|
+
|
|
24
|
+
@values = if values.empty?
|
|
25
|
+
""
|
|
26
|
+
elsif values.size == 1
|
|
27
|
+
CGI.unescape(values.first[:value].to_s)
|
|
28
|
+
else
|
|
29
|
+
values.map{|e| CGI.unescape(e[:value].to_s)}
|
|
30
|
+
end
|
|
31
|
+
else # No values for the operand
|
|
32
|
+
@name = triad[:name].to_sym
|
|
33
|
+
@op = triad[:op].to_s
|
|
34
|
+
if ['!','!!'].include?(@op)
|
|
35
|
+
@values = nil
|
|
36
|
+
else
|
|
37
|
+
# Value operand without value? => convert it to empty string
|
|
38
|
+
raise "Interesting, didn't know this could happen. Oops!" if triad[:value].is_a?(Array) && !triad[:value].empty?
|
|
39
|
+
@values = (triad[:value] == []) ? '' : CGI.unescape(triad[:value].to_s) # TODO: could this be an array (or it always comes the other if)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def flattened_conditions
|
|
45
|
+
[{name: @name, op: @op, values: @values, node_object: self}]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def dump
|
|
49
|
+
vals = if values.is_a? Array
|
|
50
|
+
"[#{values.join(',')}]" # Purposedly enclose in brackets to make sure we differentiate
|
|
51
|
+
else
|
|
52
|
+
(values == '') ? "\"#{values}\"" : values # Dump the empty string explicitly with quotes if we've converted no value to empty string
|
|
53
|
+
end
|
|
54
|
+
"#{name}#{op}#{vals}"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# An Object that represents an AST tree for either an OR or an AND conditions
|
|
59
|
+
# to be applied to its items children
|
|
60
|
+
class ConditionGroup
|
|
61
|
+
attr_reader :items, :type
|
|
62
|
+
attr_accessor :parent_group
|
|
63
|
+
attr_accessor :associated_query # Metadata to be used by whomever is manipulating this
|
|
64
|
+
|
|
65
|
+
def self.load(node)
|
|
66
|
+
unless node[:o]
|
|
67
|
+
loaded = Condition.new(triad: node[:triad], parent_group: nil)
|
|
68
|
+
else
|
|
69
|
+
compactedl = compress_tree(node: node[:l], op: node[:o])
|
|
70
|
+
compactedr = compress_tree(node: node[:r], op: node[:o])
|
|
71
|
+
compacted = {op: node[:o], items: compactedl + compactedr }
|
|
72
|
+
|
|
73
|
+
loaded = ConditionGroup.new(**compacted, parent_group: nil)
|
|
74
|
+
end
|
|
75
|
+
loaded
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def initialize(op:, items:, parent_group:)
|
|
79
|
+
@type = (op.to_s == '&') ? :and : :or
|
|
80
|
+
@items = items.map do |item|
|
|
81
|
+
if item[:op]
|
|
82
|
+
ConditionGroup.new(**item, parent_group: self)
|
|
83
|
+
else
|
|
84
|
+
Condition.new(triad: item[:triad], parent_group: self)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
@parent_group = parent_group
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def dump
|
|
91
|
+
"( " + @items.map(&:dump).join(" #{type.upcase} ") + " )"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Returns an array with flat conditions from all child triad conditions
|
|
95
|
+
def flattened_conditions
|
|
96
|
+
@items.inject([]) do |accum, item|
|
|
97
|
+
accum + item.flattened_conditions
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Given a binary tree of operand conditions, transform it to a multi-leaf tree
|
|
102
|
+
# where a single condition node has potentially multiple subtrees for the same operation (instead of 2)
|
|
103
|
+
# For example (&, (&, a, b), (|, c, d)) => (&, a, b, (|, c, d))
|
|
104
|
+
def self.compress_tree(node:, op:)
|
|
105
|
+
if node[:triad]
|
|
106
|
+
return [node]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# It is an op node
|
|
110
|
+
if node[:o] == op
|
|
111
|
+
# compatible op as parent, collect my compacted children and return them up skipping my op
|
|
112
|
+
resultl = compress_tree(node: node[:l], op: op)
|
|
113
|
+
resultr = compress_tree(node: node[:r], op: op)
|
|
114
|
+
resultl+resultr
|
|
115
|
+
else
|
|
116
|
+
collected = compress_tree(node: node, op: node[:o])
|
|
117
|
+
[{op: node[:o], items: collected }]
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
class Parser < Parslet::Parser
|
|
123
|
+
root :expression
|
|
124
|
+
rule(:lparen) { str('(') }
|
|
125
|
+
rule(:rparen) { str(')') }
|
|
126
|
+
rule(:comma) { str(',') }
|
|
127
|
+
rule(:val_operator) { str('!=') | str('>=') | str('<=') | str('=') | str('<') | str('>')}
|
|
128
|
+
rule(:noval_operator) { str('!!') | str('!')}
|
|
129
|
+
rule(:and_kw) { str('&') }
|
|
130
|
+
rule(:or_kw) { str('|') }
|
|
131
|
+
|
|
132
|
+
def infix *args
|
|
133
|
+
Infix.new(*args)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
rule(:name) { match('[a-zA-Z0-9_\.]').repeat(1) } # TODO: are these the only characters that we allow for names?
|
|
137
|
+
rule(:chars) { match('[^&|),]').repeat(0).as(:value) }
|
|
138
|
+
rule(:value) { chars >> (comma >> chars ).repeat }
|
|
139
|
+
|
|
140
|
+
rule(:triad) {
|
|
141
|
+
(name.as(:name) >> val_operator.as(:op) >> value).as(:triad) |
|
|
142
|
+
(name.as(:name) >> noval_operator.as(:op)).as(:triad) |
|
|
143
|
+
lparen >> expression >> rparen
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
rule(:expression) {
|
|
147
|
+
infix_expression(triad, [and_kw, 2, :left], [or_kw, 1, :right])
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -9,7 +9,7 @@ module Praxis
|
|
|
9
9
|
class << self
|
|
10
10
|
def for(definition)
|
|
11
11
|
Class.new(self) do
|
|
12
|
-
@
|
|
12
|
+
@filters_map = case definition
|
|
13
13
|
when Hash
|
|
14
14
|
definition
|
|
15
15
|
when Array
|
|
@@ -18,7 +18,7 @@ module Praxis
|
|
|
18
18
|
raise "Cannot use FilterQueryBuilder.of without passing an array or a hash (Got: #{definition.class.name})"
|
|
19
19
|
end
|
|
20
20
|
class << self
|
|
21
|
-
attr_reader :
|
|
21
|
+
attr_reader :filters_map
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
end
|
|
@@ -33,13 +33,18 @@ module Praxis
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
# By default we'll simply use the incoming op and value, and will map
|
|
36
|
-
# the attribute based on what's on the `
|
|
36
|
+
# the attribute based on what's on the `filters_map` definition
|
|
37
37
|
def generate(filters)
|
|
38
38
|
raise "Not refactored yet!"
|
|
39
39
|
seen_associations = Set.new
|
|
40
40
|
filters.each do |(attr, spec)|
|
|
41
|
-
column_name =
|
|
42
|
-
|
|
41
|
+
column_name = _mapped_filter(attr)
|
|
42
|
+
unless column_name
|
|
43
|
+
msg = "Filtering by #{attr} is not allowed. No implementation mapping defined for it has been found \
|
|
44
|
+
and there is not a model attribute with this name either.\n" \
|
|
45
|
+
"Please add a mapping for #{attr} in the `filters_mapping` method of the appropriate Resource class"
|
|
46
|
+
raise msg
|
|
47
|
+
end
|
|
43
48
|
if column_name.is_a?(Proc)
|
|
44
49
|
bindings = column_name.call(spec)
|
|
45
50
|
# A hash of bindings, consisting of a key with column name and a value to the query value
|
|
@@ -64,9 +69,16 @@ module Praxis
|
|
|
64
69
|
add_clause(attr: column_name, op: op, value: value)
|
|
65
70
|
end
|
|
66
71
|
|
|
67
|
-
def
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
def _mapped_filter(name)
|
|
73
|
+
target = self.class.filters_map[name]
|
|
74
|
+
unless target
|
|
75
|
+
if @model.attribute_names.include?(name.to_s)
|
|
76
|
+
# Cache it in the filters mapping (to avoid later lookups), and return it.
|
|
77
|
+
self.class.filters_map[name] = name
|
|
78
|
+
target = name
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
return target
|
|
70
82
|
end
|
|
71
83
|
|
|
72
84
|
# Private to try to funnel all column names through `generate` that restricts
|
|
@@ -7,53 +7,20 @@ module Praxis
|
|
|
7
7
|
Praxis::ActionDefinition.send(:include, ActionDefinitionExtension)
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
-
|
|
11
10
|
def expanded_fields
|
|
12
11
|
@expansion ||= request.action.expanded_fields(self.request, self.media_type)
|
|
13
12
|
end
|
|
14
13
|
|
|
15
|
-
|
|
16
14
|
module ActionDefinitionExtension
|
|
17
15
|
extend ActiveSupport::Concern
|
|
18
16
|
|
|
19
17
|
def expanded_fields(request, media_type)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
# Determine what, if any, fields to display.
|
|
24
|
-
fields = if use_fields
|
|
25
|
-
request.params.fields.fields
|
|
26
|
-
else
|
|
27
|
-
true
|
|
28
|
-
end
|
|
18
|
+
uses_fields = self.params && self.params.attributes.key?(:fields)
|
|
19
|
+
fields = uses_fields ? request.params.fields.fields : true
|
|
29
20
|
|
|
30
|
-
|
|
31
|
-
view = if use_view && (view_name = request.params.view)
|
|
32
|
-
media_type.views[view_name]
|
|
33
|
-
else
|
|
34
|
-
media_type.views[:default]
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
expandable = if fields == true
|
|
38
|
-
# We want to show ALL of the available fields.
|
|
39
|
-
# This can never be applied to the type (it's likely infinitely recursive).
|
|
40
|
-
# So use view_name determimed above.
|
|
41
|
-
view
|
|
42
|
-
else
|
|
43
|
-
# We want to show SOME of fields available on a view or type.
|
|
44
|
-
if use_view && request.params.view
|
|
45
|
-
# Use the requested view.
|
|
46
|
-
view
|
|
47
|
-
else
|
|
48
|
-
# Use the type.
|
|
49
|
-
media_type
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
Praxis::FieldExpander.expand(expandable,fields)
|
|
21
|
+
Praxis::FieldExpander.expand(media_type,fields)
|
|
54
22
|
end
|
|
55
23
|
end
|
|
56
|
-
|
|
57
24
|
end
|
|
58
25
|
end
|
|
59
26
|
end
|
|
@@ -14,10 +14,9 @@ module Praxis
|
|
|
14
14
|
module Pagination
|
|
15
15
|
extend ActiveSupport::Concern
|
|
16
16
|
# This PaginatedController concern should be added to controllers that have actions that define the
|
|
17
|
-
# pagination and order parameters so that
|
|
18
|
-
#
|
|
19
|
-
# This
|
|
20
|
-
# can be easily applied to other chainable query proxies.
|
|
17
|
+
# pagination and order parameters so that one can call the domain model to craft the query
|
|
18
|
+
# `domain_model.craft_pagination_query(base_query, pagination: _pagination)`
|
|
19
|
+
# This will handle all the required logic for paginating, ordering and generating the Link and TotalCount headers.
|
|
21
20
|
#
|
|
22
21
|
# Here's a simple example on how to use it for a fake Items controller
|
|
23
22
|
# class Items < V1::Controllers::BaseController
|
|
@@ -29,7 +28,8 @@ module Praxis
|
|
|
29
28
|
#
|
|
30
29
|
# def index(filters: nil, pagination: nil, order: nil, **_args)
|
|
31
30
|
# items = current_user.items.all
|
|
32
|
-
#
|
|
31
|
+
# domain_model = self.media_type.domain_model
|
|
32
|
+
# items = domain_model.craft_pagination_query( query: items, pagination: _pagination)
|
|
33
33
|
#
|
|
34
34
|
# display(items)
|
|
35
35
|
# end
|
|
@@ -71,33 +71,6 @@ module Praxis
|
|
|
71
71
|
@_pagination = PaginationStruct.new(pagination[:paginator], pagination[:order])
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
# Main entrypoint: Handles all pagination pieces
|
|
75
|
-
# takes:
|
|
76
|
-
# * the query to build from and the table
|
|
77
|
-
# * the request (for link header generation)
|
|
78
|
-
# * requires the _pagination variable to be there (set by this module) to return the pagination struct
|
|
79
|
-
def handle_pagination(query:, type: :active_record)
|
|
80
|
-
handler_klass = \
|
|
81
|
-
case type
|
|
82
|
-
when :active_record
|
|
83
|
-
ActiveRecordPaginationHandler
|
|
84
|
-
when :sequel
|
|
85
|
-
SequelPaginationHandler
|
|
86
|
-
else
|
|
87
|
-
raise "Attempting to use pagination but Active Record or Sequel gems found"
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# Gather and save the count if required
|
|
91
|
-
if _pagination.paginator&.total_count
|
|
92
|
-
_pagination.total_count = handler_klass.count(query.dup)
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
query = handler_klass.order(query, _pagination.order)
|
|
96
|
-
# Maybe this is a class instance instead of a class method?...(of the appropriate AR/Sequel type)...
|
|
97
|
-
# self.class.paginate(query, table, _pagination)
|
|
98
|
-
handler_klass.paginate(query, _pagination)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
74
|
def build_pagination_headers(pagination:, current_url:, current_query_params:)
|
|
102
75
|
links = if pagination.paginator.by
|
|
103
76
|
# We're assuming that the last element has a "symbol/string" field with the same name of the "by" pagination.
|
|
@@ -115,7 +115,7 @@ module Praxis
|
|
|
115
115
|
def self.construct(pagination_definition, **options)
|
|
116
116
|
return self if pagination_definition.nil?
|
|
117
117
|
|
|
118
|
-
DSLCompiler.new(self, options).parse(*pagination_definition)
|
|
118
|
+
DSLCompiler.new(self, **options).parse(*pagination_definition)
|
|
119
119
|
self
|
|
120
120
|
end
|
|
121
121
|
|
|
@@ -202,7 +202,7 @@ module Praxis
|
|
|
202
202
|
def self.construct(pagination_definition, **options)
|
|
203
203
|
return self if pagination_definition.nil?
|
|
204
204
|
|
|
205
|
-
DSLCompiler.new(self, options).parse(*pagination_definition)
|
|
205
|
+
DSLCompiler.new(self, **options).parse(*pagination_definition)
|
|
206
206
|
self
|
|
207
207
|
end
|
|
208
208
|
|
|
@@ -221,10 +221,11 @@ module Praxis
|
|
|
221
221
|
|
|
222
222
|
selectable = mt_example.object.keys & simple_attrs.keys
|
|
223
223
|
by = selectable.sample(1).first
|
|
224
|
-
from = media_type.attributes[by].example(parent: mt_example)
|
|
224
|
+
from = media_type.attributes[by].example(parent: mt_example).to_s
|
|
225
225
|
# Make sure to encode the value of the from, as it can contain commas and such
|
|
226
|
-
|
|
227
|
-
"
|
|
226
|
+
# 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 = (from && !from.empty?) ? ",from=#{CGI.escape(from)}" : ''
|
|
228
|
+
"by=#{by}#{optional_from_component},items=#{defaults[:page_size]}"
|
|
228
229
|
else
|
|
229
230
|
"by=id,from=20,items=100"
|
|
230
231
|
end
|
|
@@ -360,6 +361,7 @@ module Praxis
|
|
|
360
361
|
else
|
|
361
362
|
s = "by=#{@by}"
|
|
362
363
|
s += ",from=#{@from}" if @from
|
|
364
|
+
s
|
|
363
365
|
end
|
|
364
366
|
str += ",items=#{items}" if @items
|
|
365
367
|
str += ",total_count=true" if @total_count
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module Praxis
|
|
3
|
+
class FieldExpander
|
|
4
|
+
def self.expand(object, fields = true)
|
|
5
|
+
new.expand(object, fields)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
attr_reader :stack
|
|
9
|
+
attr_reader :history
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@stack = Hash.new do |hash, key|
|
|
13
|
+
hash[key] = Set.new
|
|
14
|
+
end
|
|
15
|
+
@history = Hash.new do |hash, key|
|
|
16
|
+
hash[key] = {}
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def expand(object, fields = true)
|
|
21
|
+
if stack[object].include? fields
|
|
22
|
+
return history[object][fields] if history[object].include? fields
|
|
23
|
+
# We should probably never get here, since we should have a record
|
|
24
|
+
# of the history of an expansion if we're trying to redo it,
|
|
25
|
+
# but we should also be conservative and raise here just in case.
|
|
26
|
+
raise "Circular expansion detected for object #{object.inspect} with fields #{fields.inspect}"
|
|
27
|
+
else
|
|
28
|
+
stack[object] << fields
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
result = if object.is_a? Attributor::Attribute
|
|
32
|
+
expand_type(object.type, fields)
|
|
33
|
+
else
|
|
34
|
+
expand_type(object, fields)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
result
|
|
38
|
+
ensure
|
|
39
|
+
stack[object].delete fields
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def expand_fields(attributes, fields)
|
|
43
|
+
raise ArgumentError, 'expand_fields must be given a block' unless block_given?
|
|
44
|
+
|
|
45
|
+
unless fields == true
|
|
46
|
+
attributes = attributes.select do |k, _v|
|
|
47
|
+
fields.key?(k)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
attributes.each_with_object({}) do |(name, dumpable), hash|
|
|
52
|
+
sub_fields = case fields
|
|
53
|
+
when true
|
|
54
|
+
true
|
|
55
|
+
when Hash
|
|
56
|
+
fields[name] || true
|
|
57
|
+
end
|
|
58
|
+
hash[name] = yield(dumpable, sub_fields)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def expand_type(object, fields = true)
|
|
63
|
+
unless object.respond_to?(:attributes)
|
|
64
|
+
if object.respond_to?(:member_attribute)
|
|
65
|
+
return expand_type(object.member_attribute.type, fields)
|
|
66
|
+
else
|
|
67
|
+
return true
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# just include the full thing if it has no attributes
|
|
72
|
+
return true if object.attributes.empty?
|
|
73
|
+
|
|
74
|
+
# True, expands to the default fieldset for blueprints
|
|
75
|
+
fields = object.default_fieldset if object < Praxis::Blueprint && fields == true
|
|
76
|
+
|
|
77
|
+
return history[object][fields] if history[object].include? fields
|
|
78
|
+
|
|
79
|
+
history[object][fields] = {}
|
|
80
|
+
result = expand_fields(object.attributes, fields) do |dumpable, sub_fields|
|
|
81
|
+
expand(dumpable.type, sub_fields)
|
|
82
|
+
end
|
|
83
|
+
unless fields == true
|
|
84
|
+
non_matching = fields.keys - object.attributes.keys
|
|
85
|
+
raise "FieldExpansion error: attribute(s) #{non_matching} do not exist in #{object}" unless non_matching.empty?
|
|
86
|
+
end
|
|
87
|
+
history[object][fields].merge!(result)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|