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.
Files changed (235) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +54 -0
  3. data/.simplecov +3 -1
  4. data/.travis.yml +2 -1
  5. data/CHANGELOG.md +19 -0
  6. data/CONTRIBUTING.md +2 -79
  7. data/Gemfile +5 -1
  8. data/Guardfile +6 -4
  9. data/LICENSE +0 -2
  10. data/MAINTAINERS.md +1 -0
  11. data/README.md +15 -22
  12. data/Rakefile +4 -2
  13. data/bin/praxis +55 -58
  14. data/lib/praxis/action_definition/headers_dsl_compiler.rb +5 -6
  15. data/lib/praxis/action_definition.rb +65 -95
  16. data/lib/praxis/api_definition.rb +21 -29
  17. data/lib/praxis/api_general_info.rb +55 -66
  18. data/lib/praxis/application.rb +15 -32
  19. data/lib/praxis/blueprint.rb +80 -73
  20. data/lib/praxis/bootloader.rb +24 -33
  21. data/lib/praxis/bootloader_stages/environment.rb +5 -10
  22. data/lib/praxis/bootloader_stages/file_loader.rb +3 -6
  23. data/lib/praxis/bootloader_stages/plugin_config_load.rb +4 -6
  24. data/lib/praxis/bootloader_stages/plugin_config_prepare.rb +2 -2
  25. data/lib/praxis/bootloader_stages/plugin_loader.rb +3 -7
  26. data/lib/praxis/bootloader_stages/plugin_setup.rb +3 -3
  27. data/lib/praxis/bootloader_stages/routing.rb +5 -8
  28. data/lib/praxis/bootloader_stages/subgroup_loader.rb +2 -10
  29. data/lib/praxis/bootloader_stages/warn_unloaded_files.rb +15 -19
  30. data/lib/praxis/callbacks.rb +12 -11
  31. data/lib/praxis/collection.rb +11 -14
  32. data/lib/praxis/config.rb +17 -28
  33. data/lib/praxis/config_hash.rb +2 -1
  34. data/lib/praxis/controller.rb +7 -6
  35. data/lib/praxis/dispatcher.rb +34 -42
  36. data/lib/praxis/docs/open_api/info_object.rb +11 -8
  37. data/lib/praxis/docs/open_api/media_type_object.rb +18 -17
  38. data/lib/praxis/docs/open_api/operation_object.rb +7 -4
  39. data/lib/praxis/docs/open_api/parameter_object.rb +17 -14
  40. data/lib/praxis/docs/open_api/paths_object.rb +11 -9
  41. data/lib/praxis/docs/open_api/request_body_object.rb +14 -13
  42. data/lib/praxis/docs/open_api/response_object.rb +24 -18
  43. data/lib/praxis/docs/open_api/responses_object.rb +3 -1
  44. data/lib/praxis/docs/open_api/schema_object.rb +61 -29
  45. data/lib/praxis/docs/open_api/server_object.rb +5 -2
  46. data/lib/praxis/docs/open_api/tag_object.rb +9 -6
  47. data/lib/praxis/docs/open_api_generator.rb +114 -150
  48. data/lib/praxis/endpoint_definition.rb +60 -77
  49. data/lib/praxis/error_handler.rb +2 -2
  50. data/lib/praxis/exception.rb +2 -0
  51. data/lib/praxis/exceptions/config.rb +3 -1
  52. data/lib/praxis/exceptions/config_load.rb +2 -0
  53. data/lib/praxis/exceptions/config_validation.rb +3 -1
  54. data/lib/praxis/exceptions/invalid_configuration.rb +3 -1
  55. data/lib/praxis/exceptions/invalid_response.rb +3 -1
  56. data/lib/praxis/exceptions/invalid_trait.rb +3 -1
  57. data/lib/praxis/exceptions/stage_not_found.rb +3 -1
  58. data/lib/praxis/exceptions/validation.rb +4 -3
  59. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +163 -149
  60. data/lib/praxis/extensions/attribute_filtering/active_record_patches/5x.rb +18 -13
  61. data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_0.rb +13 -9
  62. data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_1_plus.rb +14 -11
  63. data/lib/praxis/extensions/attribute_filtering/active_record_patches.rb +12 -9
  64. data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +8 -5
  65. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +89 -65
  66. data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +68 -62
  67. data/lib/praxis/extensions/attribute_filtering.rb +3 -1
  68. data/lib/praxis/extensions/field_expansion.rb +6 -4
  69. data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +10 -8
  70. data/lib/praxis/extensions/field_selection/field_selector.rb +91 -92
  71. data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +12 -12
  72. data/lib/praxis/extensions/field_selection.rb +3 -1
  73. data/lib/praxis/extensions/pagination/active_record_pagination_handler.rb +6 -4
  74. data/lib/praxis/extensions/pagination/header_generator.rb +16 -11
  75. data/lib/praxis/extensions/pagination/ordering_params.rb +29 -28
  76. data/lib/praxis/extensions/pagination/pagination_handler.rb +44 -42
  77. data/lib/praxis/extensions/pagination/pagination_params.rb +29 -48
  78. data/lib/praxis/extensions/pagination/sequel_pagination_handler.rb +8 -7
  79. data/lib/praxis/extensions/pagination.rb +10 -15
  80. data/lib/praxis/extensions/rails_compat/request_methods.rb +3 -4
  81. data/lib/praxis/extensions/rails_compat.rb +2 -0
  82. data/lib/praxis/extensions/rendering.rb +12 -12
  83. data/lib/praxis/field_expander.rb +8 -9
  84. data/lib/praxis/file_group.rb +8 -12
  85. data/lib/praxis/finalizable.rb +1 -0
  86. data/lib/praxis/handlers/json.rb +5 -2
  87. data/lib/praxis/handlers/plain.rb +2 -1
  88. data/lib/praxis/handlers/www_form.rb +6 -3
  89. data/lib/praxis/handlers/{xml-sample.rb → xml_sample.rb} +26 -22
  90. data/lib/praxis/mapper/active_model_compat.rb +13 -10
  91. data/lib/praxis/mapper/resource.rb +196 -181
  92. data/lib/praxis/mapper/selector_generator.rb +106 -112
  93. data/lib/praxis/mapper/sequel_compat.rb +70 -67
  94. data/lib/praxis/media_type.rb +2 -2
  95. data/lib/praxis/media_type_identifier.rb +26 -22
  96. data/lib/praxis/middleware_app.rb +18 -15
  97. data/lib/praxis/multipart/parser.rb +46 -51
  98. data/lib/praxis/multipart/part.rb +78 -110
  99. data/lib/praxis/notifications.rb +2 -4
  100. data/lib/praxis/plugin.rb +11 -18
  101. data/lib/praxis/plugin_concern.rb +12 -15
  102. data/lib/praxis/plugins/mapper_plugin.rb +15 -13
  103. data/lib/praxis/plugins/pagination_plugin.rb +8 -6
  104. data/lib/praxis/plugins/rails_plugin.rb +33 -28
  105. data/lib/praxis/renderer.rb +11 -15
  106. data/lib/praxis/request.rb +48 -44
  107. data/lib/praxis/request_stages/action.rb +4 -6
  108. data/lib/praxis/request_stages/load_request.rb +2 -4
  109. data/lib/praxis/request_stages/request_stage.rb +19 -23
  110. data/lib/praxis/request_stages/response.rb +4 -6
  111. data/lib/praxis/request_stages/validate.rb +3 -5
  112. data/lib/praxis/request_stages/validate_params_and_headers.rb +15 -22
  113. data/lib/praxis/request_stages/validate_payload.rb +25 -28
  114. data/lib/praxis/request_superclassing.rb +3 -3
  115. data/lib/praxis/resource_definition.rb +1 -0
  116. data/lib/praxis/response.rb +24 -26
  117. data/lib/praxis/response_definition.rb +77 -122
  118. data/lib/praxis/response_template.rb +11 -15
  119. data/lib/praxis/responses/http.rb +23 -44
  120. data/lib/praxis/responses/internal_server_error.rb +18 -21
  121. data/lib/praxis/responses/multipart_ok.rb +4 -9
  122. data/lib/praxis/responses/validation_error.rb +8 -15
  123. data/lib/praxis/route.rb +8 -10
  124. data/lib/praxis/router/rack.rb +13 -7
  125. data/lib/praxis/router/simple.rb +10 -5
  126. data/lib/praxis/router.rb +27 -34
  127. data/lib/praxis/routing_config.rb +52 -29
  128. data/lib/praxis/simple_media_type.rb +5 -8
  129. data/lib/praxis/stage.rb +17 -25
  130. data/lib/praxis/tasks/api_docs.rb +17 -16
  131. data/lib/praxis/tasks/console.rb +3 -1
  132. data/lib/praxis/tasks/environment.rb +2 -0
  133. data/lib/praxis/tasks/routes.rb +26 -24
  134. data/lib/praxis/tasks.rb +3 -1
  135. data/lib/praxis/trait.rb +37 -46
  136. data/lib/praxis/types/fuzzy_hash.rb +13 -14
  137. data/lib/praxis/types/media_type_common.rb +11 -10
  138. data/lib/praxis/types/multipart_array/part_definition.rb +14 -17
  139. data/lib/praxis/types/multipart_array.rb +100 -115
  140. data/lib/praxis/validation_handler.rb +5 -3
  141. data/lib/praxis/version.rb +3 -1
  142. data/lib/praxis.rb +4 -5
  143. data/praxis.gemspec +22 -21
  144. data/spec/functional_spec.rb +44 -56
  145. data/spec/praxis/action_definition_spec.rb +39 -48
  146. data/spec/praxis/api_definition_spec.rb +45 -47
  147. data/spec/praxis/api_general_info_spec.rb +28 -29
  148. data/spec/praxis/application_spec.rb +18 -14
  149. data/spec/praxis/blueprint_spec.rb +33 -34
  150. data/spec/praxis/bootloader_spec.rb +32 -30
  151. data/spec/praxis/callbacks_spec.rb +37 -37
  152. data/spec/praxis/collection_spec.rb +18 -25
  153. data/spec/praxis/config_hash_spec.rb +5 -4
  154. data/spec/praxis/config_spec.rb +27 -26
  155. data/spec/praxis/controller_spec.rb +8 -9
  156. data/spec/praxis/endpoint_definition_spec.rb +25 -32
  157. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +171 -114
  158. data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +22 -21
  159. data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +112 -60
  160. data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +37 -38
  161. data/spec/praxis/extensions/field_expansion_spec.rb +8 -10
  162. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +14 -13
  163. data/spec/praxis/extensions/field_selection/field_selector_spec.rb +9 -16
  164. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +50 -49
  165. data/spec/praxis/extensions/pagination/active_record_pagination_handler_spec.rb +32 -31
  166. data/spec/praxis/extensions/rendering_spec.rb +9 -9
  167. data/spec/praxis/extensions/support/spec_resources_active_model.rb +32 -49
  168. data/spec/praxis/extensions/support/spec_resources_sequel.rb +48 -48
  169. data/spec/praxis/field_expander_spec.rb +6 -5
  170. data/spec/praxis/file_group_spec.rb +3 -1
  171. data/spec/praxis/handlers/json_spec.rb +6 -5
  172. data/spec/praxis/mapper/resource_spec.rb +39 -29
  173. data/spec/praxis/mapper/selector_generator_spec.rb +80 -46
  174. data/spec/praxis/media_type_identifier_spec.rb +13 -10
  175. data/spec/praxis/media_type_spec.rb +12 -12
  176. data/spec/praxis/middleware_app_spec.rb +23 -22
  177. data/spec/praxis/multipart/parser_spec.rb +7 -9
  178. data/spec/praxis/notifications_spec.rb +4 -4
  179. data/spec/praxis/plugin_concern_spec.rb +5 -6
  180. data/spec/praxis/renderer_spec.rb +10 -9
  181. data/spec/praxis/request_spec.rb +38 -41
  182. data/spec/praxis/request_stages/action_spec.rb +14 -15
  183. data/spec/praxis/request_stages/request_stage_spec.rb +30 -41
  184. data/spec/praxis/request_stages/validate_spec.rb +3 -1
  185. data/spec/praxis/response_definition_spec.rb +79 -92
  186. data/spec/praxis/response_spec.rb +35 -40
  187. data/spec/praxis/responses/internal_server_error_spec.rb +6 -9
  188. data/spec/praxis/responses/validation_error_spec.rb +17 -18
  189. data/spec/praxis/route_spec.rb +4 -7
  190. data/spec/praxis/router_spec.rb +69 -79
  191. data/spec/praxis/routing_config_spec.rb +15 -14
  192. data/spec/praxis/stage_spec.rb +56 -53
  193. data/spec/praxis/trait_spec.rb +17 -17
  194. data/spec/praxis/types/fuzzy_hash_spec.rb +11 -9
  195. data/spec/praxis/types/multipart_array/part_definition_spec.rb +3 -2
  196. data/spec/praxis/types/multipart_array_spec.rb +33 -48
  197. data/spec/spec_app/app/concerns/authenticated.rb +5 -5
  198. data/spec/spec_app/app/concerns/basic_api.rb +3 -1
  199. data/spec/spec_app/app/concerns/log_wrapper.rb +5 -3
  200. data/spec/spec_app/app/controllers/base_class.rb +6 -5
  201. data/spec/spec_app/app/controllers/instances.rb +31 -34
  202. data/spec/spec_app/app/controllers/volumes.rb +6 -6
  203. data/spec/spec_app/app/responses/multipart.rb +1 -2
  204. data/spec/spec_app/app/responses/other_response.rb +2 -2
  205. data/spec/spec_app/config/environment.rb +19 -6
  206. data/spec/spec_app/config.ru +4 -3
  207. data/spec/spec_app/design/api.rb +13 -15
  208. data/spec/spec_app/design/media_types/instance.rb +6 -6
  209. data/spec/spec_app/design/media_types/volume.rb +2 -1
  210. data/spec/spec_app/design/media_types/volume_snapshot.rb +2 -1
  211. data/spec/spec_app/design/resources/instances.rb +11 -17
  212. data/spec/spec_app/design/resources/volume_snapshots.rb +4 -5
  213. data/spec/spec_app/design/resources/volumes.rb +4 -5
  214. data/spec/spec_helper.rb +11 -13
  215. data/spec/support/be_deep_equal_matcher.rb +5 -0
  216. data/spec/support/spec_authorization_plugin.rb +7 -12
  217. data/spec/support/spec_blueprints.rb +5 -4
  218. data/spec/support/spec_complex_authentication_plugin.rb +17 -34
  219. data/spec/support/spec_endpoint_definitions.rb +2 -3
  220. data/spec/support/spec_media_types.rb +28 -35
  221. data/spec/support/spec_resources.rb +22 -16
  222. data/spec/support/spec_simple_authentication_plugin.rb +5 -9
  223. data/tasks/loader.thor +4 -2
  224. data/tasks/thor/app.rb +7 -5
  225. data/tasks/thor/example.rb +23 -22
  226. data/tasks/thor/model.rb +7 -7
  227. data/tasks/thor/scaffold.rb +23 -23
  228. data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +0 -8
  229. data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +1 -2
  230. metadata +72 -84
  231. data/MAINTAINERS +0 -2
  232. data/TODO.md +0 -25
  233. data/spec/praxis/api_resource_spec.rb +0 -0
  234. data/spec/praxis/dispatcher_spec.rb +0 -0
  235. data/spec/spec_app/app/responses/bulk_response.rb +0 -0
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # This is an example of a handler that can load and generate 'activesupport-style' xml payloads.
2
- # Note that if you use your API to pass nil values for attributes as a way to unset their values,
3
- # this handler will not work (as there isn't necessarily a defined "null" value in this encoding
4
+ # Note that if you use your API to pass nil values for attributes as a way to unset their values,
5
+ # this handler will not work (as there isn't necessarily a defined "null" value in this encoding
4
6
  # (although you can probably define how to encode/decode it and use it as such)
5
7
  # Use at your own risk
6
8
 
@@ -17,7 +19,7 @@ module Praxis
17
19
  ActiveSupport::XmlMini.backend = 'Nokogiri'
18
20
  rescue LoadError
19
21
  raise Praxis::Exceptions::InvalidConfiguration,
20
- "XML handler depends on builder ~> 3.2 and nokogiri ~> 1.6; please add them to your Gemfile"
22
+ 'XML handler depends on builder ~> 3.2 and nokogiri ~> 1.6; please add them to your Gemfile'
21
23
  end
22
24
 
23
25
  # Parse an XML document into structured data.
@@ -46,35 +48,37 @@ module Praxis
46
48
 
47
49
  case type
48
50
  when nil
49
- if (node.children.size == 1 && node.child.text?) || node.children.size == 0
51
+ if (node.children.size == 1 && node.child.text?) || node.children.size.zero?
50
52
  # leaf text node
51
- return node.content
53
+ node.content
52
54
  else
53
55
  # A hash
54
- return node.children.each_with_object({}) do |child, hash|
56
+ node.children.each_with_object({}) do |child, hash|
55
57
  next unless child.element? # There might be text fragments like newlines...spaces
58
+
56
59
  hash[child.name.underscore] = process(child, child.attributes['type'])
57
60
  end
58
61
  end
59
- when "array"
60
- return node.children.each_with_object([]) do |child, arr|
62
+ when 'array'
63
+ node.children.each_with_object([]) do |child, arr|
61
64
  next unless child.element? # There might be text fragments like newlines...spaces
65
+
62
66
  arr << process(child, child.attributes['type'])
63
67
  end
64
- when "integer"
65
- return Integer(node.content)
66
- when "symbol"
67
- return node.content.to_sym
68
- when "decimal"
69
- return BigDecimal(node.content)
70
- when "float"
71
- return Float(node.content)
72
- when "boolean"
73
- return ((node.content == "false") ? false : true)
74
- when "date"
75
- return Date.parse(node.content)
76
- when "dateTime"
77
- return DateTime.parse(node.content)
68
+ when 'integer'
69
+ Integer(node.content)
70
+ when 'symbol'
71
+ node.content.to_sym
72
+ when 'decimal'
73
+ BigDecimal(node.content)
74
+ when 'float'
75
+ Float(node.content)
76
+ when 'boolean'
77
+ node.content != 'false'
78
+ when 'date'
79
+ Date.parse(node.content)
80
+ when 'dateTime'
81
+ DateTime.parse(node.content)
78
82
  else
79
83
  raise ArgumentError, "Unknown attribute type: #{type}"
80
84
  end
@@ -28,7 +28,7 @@ module Praxis
28
28
  end
29
29
 
30
30
  def _praxis_associations
31
- orig = self.reflections.clone
31
+ orig = reflections.clone
32
32
 
33
33
  orig.each_with_object({}) do |(k, v), hash|
34
34
  # Assume an 'id' primary key if the system is initializing without AR connected
@@ -51,7 +51,7 @@ module Praxis
51
51
  else
52
52
  raise "Unknown association type: #{v.class.name} on #{v.klass.name} for #{v.name}"
53
53
  end
54
- # Call out any local (i.e., of this model) columns that participate in the association
54
+ # Call out any local (i.e., of this model) columns that participate in the association
55
55
  info[:local_key_columns] = local_columns_used_for_the_association(info[:type], v)
56
56
  info[:remote_key_columns] = remote_columns_used_for_the_association(info[:type], v)
57
57
 
@@ -63,8 +63,8 @@ module Praxis
63
63
  end
64
64
 
65
65
  def _join_foreign_key_for(assoc_reflection)
66
- maj, min, _ = ActiveRecord.gem_version.segments
67
- if maj >= 6 && min >=1
66
+ maj, min, = ActiveRecord.gem_version.segments
67
+ if maj >= 6 && min >= 1
68
68
  assoc_reflection.join_foreign_key.to_sym
69
69
  else
70
70
  assoc_reflection.join_keys.foreign_key.to_sym
@@ -72,14 +72,16 @@ module Praxis
72
72
  end
73
73
 
74
74
  def _join_primary_key_for(assoc_reflection)
75
- maj, min, _ = ActiveRecord.gem_version.segments
76
- if maj >= 6 && min >=1
75
+ maj, min, = ActiveRecord.gem_version.segments
76
+ if maj >= 6 && min >= 1
77
77
  assoc_reflection.join_primary_key.to_sym
78
78
  else
79
79
  assoc_reflection.join_keys.key.to_sym
80
80
  end
81
81
  end
82
+
82
83
  private
84
+
83
85
  def local_columns_used_for_the_association(type, assoc_reflection)
84
86
  case type
85
87
  when :one_to_many
@@ -92,7 +94,7 @@ module Praxis
92
94
  ref = resolve_closest_through_reflection(assoc_reflection)
93
95
  # The associated middle table will point to us by key (usually the PK, but not always)
94
96
  [_join_foreign_key_for(ref)] # The foreign key that the last through table points to
95
- else
97
+ else
96
98
  raise "association type #{type} not supported"
97
99
  end
98
100
  end
@@ -103,16 +105,17 @@ module Praxis
103
105
  case type
104
106
  when :one_to_many, :many_to_one, :many_to_many
105
107
  [_join_primary_key_for(assoc_reflection)]
106
- else
108
+ else
107
109
  raise "association type #{type} not supported"
108
110
  end
109
111
  end
110
-
112
+
111
113
  # Keep following the association reflections as long as there are middle ones (i.e., through)
112
114
  # until we come to the one next to the source
113
115
  def resolve_closest_through_reflection(ref)
114
116
  return ref unless ref.through_reflection?
115
- resolve_closest_through_reflection( ref.through_reflection )
117
+
118
+ resolve_closest_through_reflection(ref.through_reflection)
116
119
  end
117
120
  end
118
121
  end
@@ -1,176 +1,178 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # A resource creates a data store and instantiates a list of models that it wishes to load, building up the overall set of data that it will need.
2
4
  # Once that is complete, the data set is iterated and a resultant view is generated.
3
- module Praxis::Mapper
4
-
5
- class Resource
6
- extend Praxis::Finalizable
5
+ module Praxis
6
+ module Mapper
7
+ class Resource
8
+ extend Praxis::Finalizable
7
9
 
8
- attr_accessor :record
10
+ attr_accessor :record
9
11
 
10
- @properties = {}
12
+ @properties = {}
11
13
 
12
- class << self
13
- attr_reader :model_map
14
- attr_reader :properties
15
- end
14
+ class << self
15
+ attr_reader :model_map, :properties
16
+ # Names of the memoizable things (without the @__ prefix)
17
+ attr_accessor :memoized_variables
18
+ end
16
19
 
17
- # TODO: also support an attribute of sorts on the versioned resource module. ie, V1::Resources.api_version.
18
- # replacing the self.superclass == Praxis::Mapper::Resource condition below.
19
- def self.inherited(klass)
20
- super
21
-
22
- klass.instance_eval do
23
- # It is expected that each versioned set of resources
24
- # will have a common Base class, and so should share
25
- # a model_map
26
- if self.superclass == Praxis::Mapper::Resource
27
- @model_map = Hash.new
28
- else
29
- @model_map = self.superclass.model_map
30
- end
20
+ # TODO: also support an attribute of sorts on the versioned resource module. ie, V1::Resources.api_version.
21
+ # replacing the self.superclass == Praxis::Mapper::Resource condition below.
22
+ def self.inherited(klass)
23
+ super
31
24
 
32
- @properties = self.superclass.properties.clone
33
- @_filters_map = {}
25
+ klass.instance_eval do
26
+ # It is expected that each versioned set of resources
27
+ # will have a common Base class, and so should share
28
+ # a model_map
29
+ @model_map = if superclass == Praxis::Mapper::Resource
30
+ {}
31
+ else
32
+ superclass.model_map
33
+ end
34
+
35
+ @properties = superclass.properties.clone
36
+ @_filters_map = {}
37
+ @memoized_variables = []
38
+ end
34
39
  end
35
40
 
36
- end
41
+ # TODO: Take symbol/string and resolve the klass (but lazily, so we don't care about load order)
42
+ def self.model(klass = nil)
43
+ if klass
44
+ raise "Model #{klass.name} must be compatible with Praxis. Use ActiveModelCompat or similar compatability plugin." unless klass.methods.include?(:_praxis_associations)
37
45
 
38
- #TODO: Take symbol/string and resolve the klass (but lazily, so we don't care about load order)
39
- def self.model(klass=nil)
40
- if klass
41
- raise "Model #{klass.name} must be compatible with Praxis. Use ActiveModelCompat or similar compatability plugin." unless klass.methods.include?(:_praxis_associations)
42
- @model = klass
43
- self.model_map[klass] = self
44
- else
45
- @model
46
+ @model = klass
47
+ model_map[klass] = self
48
+ else
49
+ @model
50
+ end
46
51
  end
47
- end
48
52
 
49
- def self.property(name, dependencies: nil, through: nil)
50
- self.properties[name] = {dependencies: dependencies, through: through}
51
- end
53
+ def self.property(name, dependencies: nil, through: nil)
54
+ properties[name] = { dependencies: dependencies, through: through }
55
+ end
52
56
 
53
- def self._finalize!
54
- finalize_resource_delegates
55
- define_model_accessors
57
+ def self._finalize!
58
+ finalize_resource_delegates
59
+ define_model_accessors
56
60
 
57
- super
58
- end
61
+ super
62
+ end
59
63
 
60
- def self.finalize_resource_delegates
61
- return unless @resource_delegates
64
+ def self.finalize_resource_delegates
65
+ return unless @resource_delegates
62
66
 
63
- @resource_delegates.each do |record_name, record_attributes|
64
- record_attributes.each do |record_attribute|
65
- self.define_resource_delegate(record_name, record_attribute)
67
+ @resource_delegates.each do |record_name, record_attributes|
68
+ record_attributes.each do |record_attribute|
69
+ define_resource_delegate(record_name, record_attribute)
70
+ end
66
71
  end
67
72
  end
68
- end
69
-
70
73
 
71
- def self.define_model_accessors
72
- return if model.nil?
74
+ def self.define_model_accessors
75
+ return if model.nil?
73
76
 
74
- model._praxis_associations.each do |k,v|
75
- unless self.instance_methods.include? k
76
- define_model_association_accessor(k,v)
77
+ model._praxis_associations.each do |k, v|
78
+ define_model_association_accessor(k, v) unless instance_methods.include? k
77
79
  end
78
80
  end
79
- end
80
81
 
81
- def self.for_record(record)
82
- return record._resource if record._resource
82
+ def self.for_record(record)
83
+ return record._resource if record._resource
83
84
 
84
- if resource_class_for_record = model_map[record.class]
85
- return record._resource = resource_class_for_record.new(record)
86
- else
87
- version = self.name.split("::")[0..-2].join("::")
88
- resource_name = record.class.name.split("::").last
85
+ if (resource_class_for_record = model_map[record.class])
86
+ record._resource = resource_class_for_record.new(record)
87
+ else
88
+ version = name.split('::')[0..-2].join('::')
89
+ resource_name = record.class.name.split('::').last
89
90
 
90
- raise "No resource class corresponding to the model class '#{record.class}' is defined. (Did you forget to define '#{version}::#{resource_name}'?)"
91
+ raise "No resource class corresponding to the model class '#{record.class}' is defined. (Did you forget to define '#{version}::#{resource_name}'?)"
92
+ end
91
93
  end
92
- end
93
-
94
94
 
95
- def self.wrap(records)
96
- if records.nil?
97
- return []
98
- elsif( records.is_a?(Enumerable) )
99
- return records.compact.map { |record| self.for_record(record) }
100
- elsif ( records.respond_to?(:to_a) )
101
- return records.to_a.compact.map { |record| self.for_record(record) }
102
- else
103
- return self.for_record(records)
95
+ def self.wrap(records)
96
+ if records.nil?
97
+ []
98
+ elsif records.is_a?(Enumerable)
99
+ records.compact.map { |record| for_record(record) }
100
+ elsif records.respond_to?(:to_a)
101
+ records.to_a.compact.map { |record| for_record(record) }
102
+ else
103
+ for_record(records)
104
+ end
104
105
  end
105
- end
106
-
107
106
 
108
- def self.get(condition)
109
- record = self.model.get(condition)
107
+ def self.get(condition)
108
+ record = model.get(condition)
110
109
 
111
- self.wrap(record)
112
- end
113
-
114
- def self.all(condition={})
115
- records = self.model.all(condition)
110
+ wrap(record)
111
+ end
116
112
 
117
- self.wrap(records)
118
- end
113
+ def self.all(condition = {})
114
+ records = model.all(condition)
119
115
 
116
+ wrap(records)
117
+ end
120
118
 
121
- def self.resource_delegates
122
- @resource_delegates ||= {}
123
- end
119
+ def self.resource_delegates
120
+ @resource_delegates ||= {}
121
+ end
124
122
 
125
- def self.resource_delegate(spec)
126
- spec.each do |resource_name, attributes|
127
- resource_delegates[resource_name] = attributes
123
+ def self.resource_delegate(spec)
124
+ spec.each do |resource_name, attributes|
125
+ resource_delegates[resource_name] = attributes
126
+ end
128
127
  end
129
- end
130
128
 
131
- # Defines wrappers for model associations that return Resources
132
- def self.define_model_association_accessor(name, association_spec)
133
- association_model = association_spec.fetch(:model)
134
- association_resource_class = model_map[association_model]
129
+ # Defines wrappers for model associations that return Resources
130
+ def self.define_model_association_accessor(name, association_spec)
131
+ association_model = association_spec.fetch(:model)
132
+ association_resource_class = model_map[association_model]
133
+
134
+ return unless association_resource_class
135
135
 
136
- if association_resource_class
136
+ memoized_variables << name
137
137
  module_eval <<-RUBY, __FILE__, __LINE__ + 1
138
138
  def #{name}
139
+ return @__#{name} if instance_variable_defined?("@__#{name}")
140
+
139
141
  records = record.#{name}
140
- return nil if records.nil?
142
+ return nil if records.nil?
141
143
  @__#{name} ||= #{association_resource_class}.wrap(records)
142
144
  end
143
145
  RUBY
144
146
  end
145
- end
146
147
 
147
- def self.define_resource_delegate(resource_name, resource_attribute)
148
- related_model = model._praxis_associations[resource_name][:model]
149
- related_association = related_model._praxis_associations[resource_attribute]
148
+ def self.define_resource_delegate(resource_name, resource_attribute)
149
+ related_model = model._praxis_associations[resource_name][:model]
150
+ related_association = related_model._praxis_associations[resource_attribute]
150
151
 
151
- if related_association
152
- self.define_delegation_for_related_association(resource_name, resource_attribute, related_association)
153
- else
154
- self.define_delegation_for_related_attribute(resource_name, resource_attribute)
152
+ if related_association
153
+ define_delegation_for_related_association(resource_name, resource_attribute, related_association)
154
+ else
155
+ define_delegation_for_related_attribute(resource_name, resource_attribute)
156
+ end
155
157
  end
156
- end
157
-
158
158
 
159
- def self.define_delegation_for_related_attribute(resource_name, resource_attribute)
160
- module_eval <<-RUBY, __FILE__, __LINE__ + 1
159
+ def self.define_delegation_for_related_attribute(resource_name, resource_attribute)
160
+ memoized_variables << resource_attribute
161
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
161
162
  def #{resource_attribute}
162
163
  @__#{resource_attribute} ||= if (rec = self.#{resource_name})
163
164
  rec.#{resource_attribute}
164
165
  end
165
166
  end
166
- RUBY
167
- end
167
+ RUBY
168
+ end
168
169
 
169
- def self.define_delegation_for_related_association(resource_name, resource_attribute, related_association)
170
- related_resource_class = model_map[related_association[:model]]
171
- return unless related_resource_class
170
+ def self.define_delegation_for_related_association(resource_name, resource_attribute, related_association)
171
+ related_resource_class = model_map[related_association[:model]]
172
+ return unless related_resource_class
172
173
 
173
- module_eval <<-RUBY, __FILE__, __LINE__ + 1
174
+ memoized_variables << resource_attribute
175
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
174
176
  def #{resource_attribute}
175
177
  @__#{resource_attribute} ||= if (rec = self.#{resource_name})
176
178
  if (related = rec.#{resource_attribute})
@@ -178,88 +180,101 @@ module Praxis::Mapper
178
180
  end
179
181
  end
180
182
  end
181
- RUBY
182
- end
183
-
184
- def self.define_accessor(name)
185
- if name.to_s =~ /\?/
186
- ivar_name = "is_#{name.to_s[0..-2]}"
187
- else
188
- ivar_name = "#{name}"
183
+ RUBY
189
184
  end
190
185
 
191
- module_eval <<-RUBY, __FILE__, __LINE__ + 1
186
+ def self.define_accessor(name)
187
+ ivar_name = if name.to_s =~ /\?/
188
+ "is_#{name.to_s[0..-2]}"
189
+ else
190
+ name.to_s
191
+ end
192
+ memoized_variables << ivar_name
193
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
192
194
  def #{name}
193
- return @__#{ivar_name} if defined? @__#{ivar_name}
195
+ return @__#{ivar_name} if instance_variable_defined?("@__#{ivar_name}")
194
196
  @__#{ivar_name} = record.#{name}
195
197
  end
196
- RUBY
197
- end
198
+ RUBY
199
+ end
198
200
 
199
- # TODO: this shouldn't be needed if we incorporate it with the properties of the mapper...
200
- # ...maybe what this means is that we can change it for a better DSL in the resource?
201
- def self.filters_mapping(definition={})
202
- @_filters_map = \
203
- case definition
204
- when Hash
205
- definition
206
- when Array
207
- definition.each_with_object({}) { |item, hash| hash[item.to_sym] = item }
208
- else
209
- raise "Resource.filters_mapping only allows a hash or an array"
201
+ # TODO: this shouldn't be needed if we incorporate it with the properties of the mapper...
202
+ # ...maybe what this means is that we can change it for a better DSL in the resource?
203
+ def self.filters_mapping(definition = {})
204
+ @_filters_map = \
205
+ case definition
206
+ when Hash
207
+ definition
208
+ when Array
209
+ definition.each_with_object({}) { |item, hash| hash[item.to_sym] = item }
210
+ else
211
+ raise 'Resource.filters_mapping only allows a hash or an array'
212
+ end
213
+ end
214
+
215
+ def self.craft_filter_query(base_query, filters:)
216
+ if filters
217
+ raise "To use API filtering, you must define the mapping of api-names to resource properties (using the `filters_mapping` method in #{self})" unless @_filters_map
218
+
219
+ debug = Praxis::Application.instance.config.mapper.debug_queries
220
+ base_query = model._filter_query_builder_class.new(query: base_query, model: model, filters_map: @_filters_map, debug: debug).generate(filters)
210
221
  end
211
- end
212
222
 
213
- def self.craft_filter_query(base_query, filters:) # rubocop:disable Metrics/AbcSize
214
- if filters
215
- unless @_filters_map
216
- raise "To use API filtering, you must define the mapping of api-names to resource properties (using the `filters_mapping` method in #{self})"
223
+ base_query
224
+ end
225
+
226
+ def self.craft_field_selection_query(base_query, selectors:)
227
+ if selectors && model._field_selector_query_builder_class
228
+ debug = Praxis::Application.instance.config.mapper.debug_queries
229
+ base_query = model._field_selector_query_builder_class.new(query: base_query, selectors: selectors, debug: debug).generate
217
230
  end
218
- debug = Praxis::Application.instance.config.mapper.debug_queries
219
- base_query = model._filter_query_builder_class.new(query: base_query, model: model, filters_map: @_filters_map, debug: debug).generate(filters)
231
+
232
+ base_query
220
233
  end
221
-
222
- base_query
223
- end
224
234
 
225
- def self.craft_field_selection_query(base_query, selectors:) # rubocop:disable Metrics/AbcSize
226
- if selectors && model._field_selector_query_builder_class
227
- debug = Praxis::Application.instance.config.mapper.debug_queries
228
- base_query = model._field_selector_query_builder_class.new(query: base_query, selectors: selectors, debug: debug).generate
235
+ def self.craft_pagination_query(base_query, pagination:)
236
+ handler_klass = model._pagination_query_builder_class
237
+ return base_query unless handler_klass && (pagination.paginator || pagination.order)
238
+
239
+ # Gather and save the count if required
240
+ pagination.total_count = handler_klass.count(base_query.dup) if pagination.paginator&.total_count
241
+
242
+ base_query = handler_klass.order(base_query, pagination.order)
243
+ handler_klass.paginate(base_query, pagination)
229
244
  end
230
-
231
- base_query
232
- end
233
245
 
234
- def self.craft_pagination_query(base_query, pagination: ) # rubocop:disable Metrics/AbcSize
235
- handler_klass = model._pagination_query_builder_class
236
- return base_query unless (handler_klass && (pagination.paginator || pagination.order))
246
+ def initialize(record)
247
+ @record = record
248
+ end
237
249
 
238
- # Gather and save the count if required
239
- if pagination.paginator&.total_count
240
- pagination.total_count = handler_klass.count(base_query.dup)
250
+ def reload
251
+ clear_memoization
252
+ reload_record
241
253
  end
242
-
243
- base_query = handler_klass.order(base_query, pagination.order)
244
- handler_klass.paginate(base_query, pagination)
245
- end
246
254
 
247
- def initialize(record)
248
- @record = record
249
- end
255
+ def clear_memoization
256
+ self.class.memoized_variables.each do |name|
257
+ ivar = "@__#{name}"
258
+ remove_instance_variable(ivar) if instance_variable_defined?(ivar)
259
+ end
260
+ end
250
261
 
251
- def respond_to_missing?(name,*)
252
- @record.respond_to?(name) || super
253
- end
262
+ def reload_record
263
+ record.reload
264
+ end
254
265
 
255
- def method_missing(name,*args)
256
- if @record.respond_to?(name)
257
- self.class.define_accessor(name)
258
- self.send(name)
259
- else
260
- super
266
+ def respond_to_missing?(name, *)
267
+ @record.respond_to?(name) || super
261
268
  end
262
- end
263
269
 
270
+ def method_missing(name, *args)
271
+ if @record.respond_to?(name)
272
+ self.class.define_accessor(name)
273
+ send(name)
274
+ else
275
+ super
276
+ end
277
+ end
278
+ end
264
279
  end
265
280
  end