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.
Files changed (242) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.ruby-version +1 -1
  4. data/.travis.yml +1 -3
  5. data/CHANGELOG.md +33 -0
  6. data/TODO.md +1 -4
  7. data/bin/praxis +67 -12
  8. data/lib/praxis.rb +10 -3
  9. data/lib/praxis/action_definition.rb +15 -13
  10. data/lib/praxis/action_definition/headers_dsl_compiler.rb +0 -7
  11. data/lib/praxis/api_general_info.rb +1 -1
  12. data/lib/praxis/application.rb +6 -2
  13. data/lib/praxis/blueprint.rb +357 -0
  14. data/lib/praxis/bootloader.rb +9 -3
  15. data/lib/praxis/bootloader_stages/environment.rb +16 -13
  16. data/lib/praxis/collection.rb +1 -11
  17. data/lib/praxis/config_hash.rb +44 -0
  18. data/lib/praxis/docs/{openapi → open_api}/info_object.rb +18 -10
  19. data/lib/praxis/docs/{openapi → open_api}/media_type_object.rb +0 -0
  20. data/lib/praxis/docs/{openapi → open_api}/operation_object.rb +0 -0
  21. data/lib/praxis/docs/{openapi → open_api}/parameter_object.rb +0 -0
  22. data/lib/praxis/docs/{openapi → open_api}/paths_object.rb +0 -0
  23. data/lib/praxis/docs/{openapi → open_api}/request_body_object.rb +0 -0
  24. data/lib/praxis/docs/{openapi → open_api}/response_object.rb +0 -0
  25. data/lib/praxis/docs/{openapi → open_api}/responses_object.rb +0 -0
  26. data/lib/praxis/docs/{openapi → open_api}/schema_object.rb +0 -0
  27. data/lib/praxis/docs/{openapi → open_api}/server_object.rb +0 -0
  28. data/lib/praxis/docs/{openapi → open_api}/tag_object.rb +0 -0
  29. data/lib/praxis/docs/open_api_generator.rb +91 -6
  30. data/lib/praxis/endpoint_definition.rb +273 -0
  31. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +182 -58
  32. data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +3 -2
  33. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +47 -56
  34. data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +153 -0
  35. data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +20 -8
  36. data/lib/praxis/extensions/field_expansion.rb +3 -36
  37. data/lib/praxis/extensions/pagination.rb +5 -32
  38. data/lib/praxis/extensions/pagination/ordering_params.rb +1 -1
  39. data/lib/praxis/extensions/pagination/pagination_params.rb +6 -4
  40. data/lib/praxis/field_expander.rb +90 -0
  41. data/lib/praxis/finalizable.rb +34 -0
  42. data/lib/praxis/mapper/active_model_compat.rb +4 -0
  43. data/lib/praxis/mapper/resource.rb +18 -2
  44. data/lib/praxis/mapper/selector_generator.rb +2 -1
  45. data/lib/praxis/mapper/sequel_compat.rb +7 -0
  46. data/lib/praxis/media_type.rb +3 -68
  47. data/lib/praxis/plugin_concern.rb +1 -1
  48. data/lib/praxis/plugins/mapper_plugin.rb +24 -15
  49. data/lib/praxis/plugins/pagination_plugin.rb +34 -4
  50. data/lib/praxis/renderer.rb +88 -0
  51. data/lib/praxis/request.rb +1 -1
  52. data/lib/praxis/resource_definition.rb +2 -311
  53. data/lib/praxis/response_definition.rb +2 -10
  54. data/lib/praxis/response_template.rb +3 -3
  55. data/lib/praxis/router.rb +2 -2
  56. data/lib/praxis/routing_config.rb +1 -1
  57. data/lib/praxis/tasks/api_docs.rb +17 -64
  58. data/lib/praxis/tasks/routes.rb +1 -1
  59. data/lib/praxis/types/media_type_common.rb +1 -11
  60. data/lib/praxis/version.rb +1 -1
  61. data/praxis.gemspec +0 -1
  62. data/spec/functional_spec.rb +5 -9
  63. data/spec/praxis/action_definition_spec.rb +12 -20
  64. data/spec/praxis/blueprint_spec.rb +373 -0
  65. data/spec/praxis/bootloader_spec.rb +10 -2
  66. data/spec/praxis/collection_spec.rb +0 -13
  67. data/spec/praxis/config_hash_spec.rb +64 -0
  68. data/spec/praxis/{resource_definition_spec.rb → endpoint_definition_spec.rb} +37 -64
  69. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +249 -168
  70. data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +25 -6
  71. data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +190 -8
  72. data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +140 -0
  73. data/spec/praxis/extensions/field_expansion_spec.rb +5 -24
  74. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +1 -1
  75. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +1 -1
  76. data/spec/praxis/extensions/support/spec_resources_active_model.rb +1 -1
  77. data/spec/praxis/field_expander_spec.rb +149 -0
  78. data/spec/praxis/mapper/selector_generator_spec.rb +1 -1
  79. data/spec/praxis/media_type_identifier_spec.rb +5 -4
  80. data/spec/praxis/media_type_spec.rb +4 -93
  81. data/spec/praxis/renderer_spec.rb +188 -0
  82. data/spec/praxis/response_definition_spec.rb +0 -31
  83. data/spec/praxis/response_spec.rb +1 -1
  84. data/spec/praxis/router_spec.rb +8 -8
  85. data/spec/praxis/routing_config_spec.rb +3 -3
  86. data/spec/spec_app/app/controllers/instances.rb +13 -7
  87. data/spec/spec_app/design/media_types/instance.rb +1 -19
  88. data/spec/spec_app/design/media_types/volume.rb +1 -1
  89. data/spec/spec_app/design/media_types/volume_snapshot.rb +2 -14
  90. data/spec/spec_app/design/resources/instances.rb +5 -8
  91. data/spec/spec_app/design/resources/volume_snapshots.rb +1 -1
  92. data/spec/spec_app/design/resources/volumes.rb +1 -1
  93. data/spec/support/spec_authorization_plugin.rb +1 -1
  94. data/spec/support/spec_blueprints.rb +72 -0
  95. data/spec/support/{spec_resource_definitions.rb → spec_endpoint_definitions.rb} +2 -2
  96. data/spec/support/spec_media_types.rb +6 -26
  97. data/tasks/thor/app.rb +8 -34
  98. data/tasks/thor/example.rb +51 -285
  99. data/tasks/thor/model.rb +40 -0
  100. data/tasks/thor/scaffold.rb +117 -0
  101. data/tasks/thor/templates/generator/empty_app/.gitignore +0 -1
  102. data/tasks/thor/templates/generator/empty_app/Gemfile +7 -23
  103. data/tasks/thor/templates/generator/empty_app/README.md +1 -1
  104. data/tasks/thor/templates/generator/empty_app/Rakefile +4 -13
  105. data/tasks/thor/templates/generator/empty_app/{design/response_templates → app/v1/resources}/.empty_directory +0 -0
  106. data/tasks/thor/templates/generator/empty_app/{design/response_templates → app/v1/resources}/.gitkeep +0 -0
  107. data/tasks/thor/templates/generator/empty_app/config/environment.rb +26 -17
  108. data/tasks/thor/templates/generator/empty_app/{design/v1/resources → config/initializers}/.empty_directory +0 -0
  109. data/tasks/thor/templates/generator/empty_app/{design/v1/resources → config/initializers}/.gitkeep +0 -0
  110. data/tasks/thor/templates/generator/empty_app/design/v1/endpoints/.empty_directory +0 -0
  111. data/tasks/thor/templates/generator/empty_app/design/v1/endpoints/.gitkeep +0 -0
  112. data/tasks/thor/templates/generator/empty_app/docs/.empty_directory +0 -0
  113. data/tasks/thor/templates/generator/empty_app/docs/.gitkeep +0 -0
  114. data/tasks/thor/templates/generator/empty_app/spec/spec_helper.rb +14 -9
  115. data/tasks/thor/templates/generator/example_app/.gitignore +1 -0
  116. data/tasks/thor/templates/generator/example_app/Gemfile +19 -0
  117. data/tasks/thor/templates/generator/example_app/Rakefile +61 -0
  118. data/tasks/thor/templates/generator/example_app/app/models/user.rb +6 -0
  119. data/tasks/thor/templates/generator/example_app/app/v1/concerns/controller_base.rb +24 -0
  120. data/tasks/thor/templates/generator/example_app/app/v1/controllers/users.rb +17 -0
  121. data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +11 -0
  122. data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +25 -0
  123. data/tasks/thor/templates/generator/example_app/config.ru +30 -0
  124. data/tasks/thor/templates/generator/example_app/config/environment.rb +41 -0
  125. data/tasks/thor/templates/generator/example_app/db/migrate/20201010101010_create_users_table.rb +12 -0
  126. data/tasks/thor/templates/generator/example_app/db/seeds.rb +6 -0
  127. data/tasks/thor/templates/generator/example_app/design/api.rb +18 -0
  128. data/tasks/thor/templates/generator/example_app/design/v1/endpoints/users.rb +37 -0
  129. data/tasks/thor/templates/generator/example_app/design/v1/media_types/user.rb +21 -0
  130. data/tasks/thor/templates/generator/example_app/spec/helpers/database_helper.rb +20 -0
  131. data/tasks/thor/templates/generator/example_app/spec/spec_helper.rb +42 -0
  132. data/tasks/thor/templates/generator/example_app/spec/v1/controllers/users_spec.rb +37 -0
  133. data/tasks/thor/templates/generator/scaffold/design/endpoints/collection.rb +98 -0
  134. data/tasks/thor/templates/generator/scaffold/design/media_types/item.rb +18 -0
  135. data/tasks/thor/templates/generator/scaffold/implementation/controllers/collection.rb +77 -0
  136. data/tasks/thor/templates/generator/scaffold/implementation/resources/base.rb +11 -0
  137. data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +45 -0
  138. data/tasks/thor/templates/generator/scaffold/models/active_record.rb +6 -0
  139. data/tasks/thor/templates/generator/scaffold/models/sequel.rb +6 -0
  140. metadata +64 -136
  141. data/lib/api_browser/.bowerrc +0 -3
  142. data/lib/api_browser/.editorconfig +0 -21
  143. data/lib/api_browser/Gruntfile.js +0 -581
  144. data/lib/api_browser/app/index.html +0 -59
  145. data/lib/api_browser/app/js/app.js +0 -48
  146. data/lib/api_browser/app/js/controllers/action.js +0 -47
  147. data/lib/api_browser/app/js/controllers/controller.js +0 -10
  148. data/lib/api_browser/app/js/controllers/menu.js +0 -93
  149. data/lib/api_browser/app/js/controllers/trait.js +0 -10
  150. data/lib/api_browser/app/js/controllers/type.js +0 -24
  151. data/lib/api_browser/app/js/directives/attribute_description.js +0 -56
  152. data/lib/api_browser/app/js/directives/attribute_table.js +0 -28
  153. data/lib/api_browser/app/js/directives/conditional_requirements.js +0 -13
  154. data/lib/api_browser/app/js/directives/fixed_if_fits.js +0 -38
  155. data/lib/api_browser/app/js/directives/highlight.js +0 -14
  156. data/lib/api_browser/app/js/directives/menu_item.js +0 -59
  157. data/lib/api_browser/app/js/directives/no_container.js +0 -8
  158. data/lib/api_browser/app/js/directives/readable_list.js +0 -87
  159. data/lib/api_browser/app/js/directives/request_examples.js +0 -31
  160. data/lib/api_browser/app/js/directives/type_placeholder.js +0 -30
  161. data/lib/api_browser/app/js/directives/url.js +0 -15
  162. data/lib/api_browser/app/js/factories/Configuration.js +0 -12
  163. data/lib/api_browser/app/js/factories/Documentation.js +0 -61
  164. data/lib/api_browser/app/js/factories/Example.js +0 -51
  165. data/lib/api_browser/app/js/factories/PageInfo.js +0 -9
  166. data/lib/api_browser/app/js/factories/normalize_attributes.js +0 -20
  167. data/lib/api_browser/app/js/factories/prepare_template.js +0 -15
  168. data/lib/api_browser/app/js/factories/template_for.js +0 -128
  169. data/lib/api_browser/app/js/filters/attribute_name.js +0 -10
  170. data/lib/api_browser/app/js/filters/friendly_json.js +0 -5
  171. data/lib/api_browser/app/js/filters/has_requirement.js +0 -14
  172. data/lib/api_browser/app/js/filters/header_info.js +0 -9
  173. data/lib/api_browser/app/js/filters/is_empty.js +0 -8
  174. data/lib/api_browser/app/js/filters/markdown.js +0 -6
  175. data/lib/api_browser/app/js/filters/resource_name.js +0 -5
  176. data/lib/api_browser/app/js/filters/tag_requirement.js +0 -13
  177. data/lib/api_browser/app/sass/modules/_body.scss +0 -40
  178. data/lib/api_browser/app/sass/modules/_cloke.scss +0 -8
  179. data/lib/api_browser/app/sass/modules/_header.scss +0 -10
  180. data/lib/api_browser/app/sass/modules/_nav.scss +0 -7
  181. data/lib/api_browser/app/sass/modules/_sidebar.scss +0 -134
  182. data/lib/api_browser/app/sass/modules/_switch.scss +0 -55
  183. data/lib/api_browser/app/sass/modules/_table.scss +0 -13
  184. data/lib/api_browser/app/sass/praxis.scss +0 -70
  185. data/lib/api_browser/app/sass/variables/_bootstrap-variables.scss +0 -774
  186. data/lib/api_browser/app/views/action.html +0 -97
  187. data/lib/api_browser/app/views/builtin/field-selector.html +0 -24
  188. data/lib/api_browser/app/views/controller.html +0 -55
  189. data/lib/api_browser/app/views/directives/attribute_description.html +0 -2
  190. data/lib/api_browser/app/views/directives/attribute_description/default.html +0 -2
  191. data/lib/api_browser/app/views/directives/attribute_description/example.html +0 -13
  192. data/lib/api_browser/app/views/directives/attribute_description/headers.html +0 -8
  193. data/lib/api_browser/app/views/directives/attribute_description/member_options.html +0 -4
  194. data/lib/api_browser/app/views/directives/attribute_description/values.html +0 -14
  195. data/lib/api_browser/app/views/directives/attribute_table.html +0 -17
  196. data/lib/api_browser/app/views/directives/menu_item.html +0 -8
  197. data/lib/api_browser/app/views/directives/url.html +0 -3
  198. data/lib/api_browser/app/views/examples/general.html +0 -26
  199. data/lib/api_browser/app/views/home.html +0 -5
  200. data/lib/api_browser/app/views/layout.html +0 -8
  201. data/lib/api_browser/app/views/menu.html +0 -42
  202. data/lib/api_browser/app/views/navbar.html +0 -9
  203. data/lib/api_browser/app/views/trait.html +0 -13
  204. data/lib/api_browser/app/views/type.html +0 -6
  205. data/lib/api_browser/app/views/type/details.html +0 -33
  206. data/lib/api_browser/app/views/types/embedded/array.html +0 -2
  207. data/lib/api_browser/app/views/types/embedded/default.html +0 -12
  208. data/lib/api_browser/app/views/types/embedded/field-selector.html +0 -13
  209. data/lib/api_browser/app/views/types/embedded/links.html +0 -11
  210. data/lib/api_browser/app/views/types/embedded/requirements.html +0 -6
  211. data/lib/api_browser/app/views/types/embedded/single_req.html +0 -9
  212. data/lib/api_browser/app/views/types/embedded/struct.html +0 -14
  213. data/lib/api_browser/app/views/types/label/link.html +0 -1
  214. data/lib/api_browser/app/views/types/label/primitive.html +0 -1
  215. data/lib/api_browser/app/views/types/label/primitive_collection.html +0 -1
  216. data/lib/api_browser/app/views/types/label/type.html +0 -1
  217. data/lib/api_browser/app/views/types/label/type_collection.html +0 -1
  218. data/lib/api_browser/app/views/types/main/array.html +0 -22
  219. data/lib/api_browser/app/views/types/main/default.html +0 -23
  220. data/lib/api_browser/app/views/types/main/hash.html +0 -23
  221. data/lib/api_browser/app/views/types/standalone/array.html +0 -3
  222. data/lib/api_browser/app/views/types/standalone/default.html +0 -18
  223. data/lib/api_browser/app/views/types/standalone/struct.html +0 -2
  224. data/lib/api_browser/bower_template.json +0 -41
  225. data/lib/api_browser/package-lock.json +0 -7110
  226. data/lib/api_browser/package.json +0 -43
  227. data/lib/praxis/docs/generator.rb +0 -243
  228. data/lib/praxis/docs/link_builder.rb +0 -30
  229. data/lib/praxis/links.rb +0 -135
  230. data/lib/praxis/types/multipart.rb +0 -109
  231. data/spec/api_browser/directives/type_placeholder_spec.js +0 -134
  232. data/spec/api_browser/factories/configuration_spec.js +0 -32
  233. data/spec/api_browser/factories/documentation_spec.js +0 -100
  234. data/spec/api_browser/factories/normalize_attributes_spec.js +0 -92
  235. data/spec/api_browser/factories/template_for_spec.js +0 -67
  236. data/spec/api_browser/filters/attribute_name_spec.js +0 -23
  237. data/spec/praxis/types/multipart_spec.rb +0 -112
  238. data/tasks/thor/templates/generator/empty_app/.rspec +0 -1
  239. data/tasks/thor/templates/generator/empty_app/Guardfile +0 -3
  240. data/tasks/thor/templates/generator/empty_app/config/rainbows.rb +0 -57
  241. data/tasks/thor/templates/generator/empty_app/docs/app.js +0 -1
  242. 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
- @attr_to_column = case definition
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 :attr_to_column
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 `attr_to_column` hash
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 = attr_to_column[attr]
42
- raise "Filtering by #{attr} not allowed (no mapping found)" unless column_name
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 attr_to_column
68
- # Class method defined by the subclassing Class (using .for)
69
- self.class.attr_to_column
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
- use_fields = self.params && self.params.attributes.key?(:fields)
21
- use_view = self.params && self.params.attributes.key?(:view)
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
- # Determine the view that COULD be applicable.
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 calling `paginate( query: <base_query>, table: <main_table_name> )`
18
- # would handle all the required logic for paginating, ordering and generating the Link and TotalCount headers.
19
- # This assumes that the query object are chainable and based on ActiveRecord at the moment (although that logic)
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
- # items = handle_pagination( query: items)
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
- from = CGI.escape(from) if from.is_a? String
227
- "by=#{by},from=#{from},items=#{defaults[:page_size]}"
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