praxis 0.21 → 2.0.pre.3

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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +8 -15
  3. data/CHANGELOG.md +328 -299
  4. data/CONTRIBUTING.md +4 -4
  5. data/README.md +11 -9
  6. data/lib/api_browser/app/js/directives/attribute_table.js +2 -1
  7. data/lib/api_browser/app/js/directives/conditional_requirements.js +13 -0
  8. data/lib/api_browser/app/js/directives/type_placeholder.js +10 -1
  9. data/lib/api_browser/app/js/factories/normalize_attributes.js +4 -2
  10. data/lib/api_browser/app/js/factories/template_for.js +5 -2
  11. data/lib/api_browser/app/js/filters/has_requirement.js +14 -0
  12. data/lib/api_browser/app/js/filters/tag_requirement.js +13 -0
  13. data/lib/api_browser/app/sass/praxis.scss +11 -0
  14. data/lib/api_browser/app/views/action.html +2 -2
  15. data/lib/api_browser/app/views/directives/attribute_description/member_options.html +2 -2
  16. data/lib/api_browser/app/views/directives/attribute_table.html +1 -1
  17. data/lib/api_browser/app/views/type.html +1 -1
  18. data/lib/api_browser/app/views/type/details.html +2 -2
  19. data/lib/api_browser/app/views/types/embedded/array.html +2 -0
  20. data/lib/api_browser/app/views/types/embedded/default.html +3 -1
  21. data/lib/api_browser/app/views/types/embedded/requirements.html +6 -0
  22. data/lib/api_browser/app/views/types/embedded/single_req.html +9 -0
  23. data/lib/api_browser/app/views/types/embedded/struct.html +14 -2
  24. data/lib/api_browser/app/views/types/standalone/array.html +1 -1
  25. data/lib/api_browser/app/views/types/standalone/struct.html +2 -1
  26. data/lib/api_browser/package.json +1 -1
  27. data/lib/praxis.rb +9 -3
  28. data/lib/praxis/action_definition.rb +1 -1
  29. data/lib/praxis/action_definition/headers_dsl_compiler.rb +1 -1
  30. data/lib/praxis/application.rb +1 -9
  31. data/lib/praxis/bootloader.rb +1 -4
  32. data/lib/praxis/config.rb +1 -1
  33. data/lib/praxis/dispatcher.rb +10 -6
  34. data/lib/praxis/docs/generator.rb +2 -1
  35. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +180 -0
  36. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +273 -0
  37. data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +125 -0
  38. data/lib/praxis/extensions/field_selection.rb +1 -9
  39. data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +51 -0
  40. data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +61 -0
  41. data/lib/praxis/extensions/rails_compat.rb +2 -0
  42. data/lib/praxis/extensions/rails_compat/request_methods.rb +19 -0
  43. data/lib/praxis/handlers/xml.rb +1 -1
  44. data/lib/praxis/mapper/active_model_compat.rb +98 -0
  45. data/lib/praxis/mapper/resource.rb +242 -0
  46. data/lib/praxis/mapper/selector_generator.rb +149 -0
  47. data/lib/praxis/mapper/sequel_compat.rb +76 -0
  48. data/lib/praxis/media_type_identifier.rb +2 -1
  49. data/lib/praxis/middleware_app.rb +20 -2
  50. data/lib/praxis/multipart/parser.rb +14 -2
  51. data/lib/praxis/notifications.rb +1 -1
  52. data/lib/praxis/plugins/mapper_plugin.rb +64 -0
  53. data/lib/praxis/plugins/rails_plugin.rb +104 -0
  54. data/lib/praxis/request.rb +7 -1
  55. data/lib/praxis/request_superclassing.rb +11 -0
  56. data/lib/praxis/resource_definition.rb +5 -5
  57. data/lib/praxis/response.rb +1 -1
  58. data/lib/praxis/route.rb +1 -1
  59. data/lib/praxis/routing_config.rb +1 -1
  60. data/lib/praxis/trait.rb +1 -1
  61. data/lib/praxis/types/media_type_common.rb +2 -2
  62. data/lib/praxis/types/multipart.rb +1 -1
  63. data/lib/praxis/types/multipart_array.rb +2 -2
  64. data/lib/praxis/types/multipart_array/part_definition.rb +1 -1
  65. data/lib/praxis/version.rb +1 -1
  66. data/praxis.gemspec +14 -13
  67. data/spec/functional_spec.rb +4 -7
  68. data/spec/praxis/action_definition_spec.rb +1 -1
  69. data/spec/praxis/application_spec.rb +1 -1
  70. data/spec/praxis/collection_spec.rb +3 -2
  71. data/spec/praxis/config_spec.rb +2 -2
  72. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +106 -0
  73. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +147 -0
  74. data/spec/praxis/extensions/field_selection/support/spec_resources_active_model.rb +130 -0
  75. data/spec/praxis/extensions/field_selection/support/spec_resources_sequel.rb +106 -0
  76. data/spec/praxis/handlers/xml_spec.rb +2 -2
  77. data/spec/praxis/mapper/resource_spec.rb +169 -0
  78. data/spec/praxis/mapper/selector_generator_spec.rb +293 -0
  79. data/spec/praxis/media_type_spec.rb +0 -10
  80. data/spec/praxis/middleware_app_spec.rb +29 -9
  81. data/spec/praxis/request_stages/action_spec.rb +8 -1
  82. data/spec/praxis/response_definition_spec.rb +7 -4
  83. data/spec/praxis/response_spec.rb +1 -1
  84. data/spec/praxis/responses/internal_server_error_spec.rb +2 -2
  85. data/spec/praxis/responses/validation_error_spec.rb +2 -2
  86. data/spec/praxis/router_spec.rb +1 -1
  87. data/spec/spec_app/app/controllers/instances.rb +1 -1
  88. data/spec/spec_app/config/environment.rb +3 -21
  89. data/spec/spec_helper.rb +11 -15
  90. data/spec/support/be_deep_equal_matcher.rb +39 -0
  91. data/spec/support/spec_resources.rb +124 -0
  92. data/tasks/thor/templates/generator/empty_app/Gemfile +3 -3
  93. metadata +102 -77
  94. data/.ruby-version +0 -1
  95. data/lib/praxis/extensions/mapper_selectors.rb +0 -16
  96. data/lib/praxis/media_type_collection.rb +0 -127
  97. data/lib/praxis/plugins/praxis_mapper_plugin.rb +0 -246
  98. data/lib/praxis/stats.rb +0 -113
  99. data/spec/praxis/media_type_collection_spec.rb +0 -157
  100. data/spec/praxis/plugins/praxis_mapper_plugin_spec.rb +0 -142
  101. data/spec/praxis/stats_spec.rb +0 -9
  102. data/spec/spec_app/app/models/person.rb +0 -3
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+ # rubocop:disable all
3
+ module Praxis
4
+ module Extensions
5
+ class SequelFilterQueryBuilder
6
+ attr_reader :query, :root
7
+
8
+ # Abstract class, which needs to be used by subclassing it through the .for method, to set the mapping of attributes
9
+ class << self
10
+ def for(definition)
11
+ Class.new(self) do
12
+ @attr_to_column = case definition
13
+ when Hash
14
+ definition
15
+ when Array
16
+ definition.each_with_object({}) { |item, hash| hash[item.to_sym] = item }
17
+ else
18
+ raise "Cannot use FilterQueryBuilder.of without passing an array or a hash (Got: #{definition.class.name})"
19
+ end
20
+ class << self
21
+ attr_reader :attr_to_column
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ # Base query to build upon
28
+ # table is necessary when use the strin queries, when the query has multiple tables involved
29
+ # (to disambiguate)
30
+ def initialize(query:, model: )
31
+ @query = query
32
+ @root = model.table_name
33
+ end
34
+
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
37
+ def build_clause(filters)
38
+ seen_associations = Set.new
39
+ filters.each do |(attr, spec)|
40
+ column_name = attr_to_column[attr]
41
+ raise "Filtering by #{attr} not allowed (no mapping found)" unless column_name
42
+ if column_name.is_a?(Proc)
43
+ bindings = column_name.call(spec)
44
+ # A hash of bindings, consisting of a key with column name and a value to the query value
45
+ bindings.each{|col,val| expand_binding(column_name: col, op: spec[:op], value: val )}
46
+ else
47
+ expand_binding(column_name: column_name, **spec)
48
+ end
49
+ end
50
+ query
51
+ end
52
+
53
+ def expand_binding(column_name:,op:,value:)
54
+ assoc_or_field, *rest = column_name.to_s.split('.')
55
+ if rest.empty?
56
+ column_name = Sequel.qualify(root,column_name)
57
+ else
58
+ puts "Adding eager graph for #{assoc_or_field} due to being used in filter"
59
+ # Ensure the joined table is aliased properly (to the association name) so we can add the condition appropriately
60
+ @query = query.eager_graph(Sequel.as(assoc_or_field.to_sym, assoc_or_field.to_sym) )
61
+ column_name = Sequel.qualify(assoc_or_field, rest.first)
62
+ end
63
+ add_clause(attr: column_name, op: op, value: value)
64
+ end
65
+
66
+ def attr_to_column
67
+ # Class method defined by the subclassing Class (using .for)
68
+ self.class.attr_to_column
69
+ end
70
+
71
+ # Private to try to funnel all column names through `build_clause` that restricts
72
+ # the attribute names better (to allow more difficult SQL injections )
73
+ private def add_clause(attr:, op:, value:)
74
+ # TODO: partial matching
75
+ #components = attr.to_s.split('.')
76
+ #attr_selector = Sequel.qualify(*components)
77
+ attr_selector = attr
78
+ # HERE!! if we have "association.name" we should properly join it ...!
79
+
80
+ #> ds.eager_graph(:device).where{{device[:name] => 'A%'}}.select(:accountID)
81
+ #=> #<Sequel::Mysql2::Dataset: "SELECT `accountID` FROM `EventData`
82
+ # LEFT OUTER JOIN `Device` AS `device` ON
83
+ # ((`device`.`accountID` = `EventData`.`accountID`) AND (`device`.`deviceID` = `EventData`.`deviceID`))
84
+ # WHERE (`device`.`name` = 'A%')">
85
+ likeval = get_like_value(value)
86
+ @query = case op
87
+ when '='
88
+ if likeval
89
+ query.where(Sequel.like(attr_selector, likeval))
90
+ else
91
+ query.where(attr_selector => value)
92
+ end
93
+ when '!='
94
+ if likeval
95
+ query.exclude(Sequel.like(attr_selector, likeval))
96
+ else
97
+ query.exclude(attr_selector => value)
98
+ end
99
+ when '>'
100
+ #query.where(Sequel.lit("#{attr_selector} > ?", value))
101
+ query.where{attr_selector > value}
102
+ when '<'
103
+ query.where{attr_selector < value}
104
+ when '>='
105
+ query.where{attr_selector >= value}
106
+ when '<='
107
+ query.where{attr_selector <= value}
108
+ else
109
+ raise "Unsupported Operator!!! #{op}"
110
+ end
111
+ end
112
+
113
+ # Returns nil if the value was not a fuzzzy pattern
114
+ def get_like_value(value)
115
+ if value.is_a?(String) && (value[-1] == '*' || value[0] == '*')
116
+ likeval = value.dup
117
+ likeval[-1] = '%' if value[-1] == '*'
118
+ likeval[0] = '%' if value[0] == '*'
119
+ likeval
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ # rubocop:enable all
@@ -1,10 +1,2 @@
1
1
  require 'attributor/extras/field_selector'
2
-
3
- require 'praxis/extensions/field_selection/field_selector'
4
-
5
- module Praxis
6
- module Extensions
7
- module FieldSelection
8
- end
9
- end
10
- end
2
+ require 'praxis/extensions/field_selection/field_selector'
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+ module Praxis
3
+ module Extensions
4
+ module FieldSelection
5
+ class ActiveRecordQuerySelector
6
+ attr_reader :selector, :query
7
+ # Gets a dataset, a selector...and should return a dataset with the selector definition applied.
8
+ def initialize(query:, selectors:)
9
+ @selector = selectors
10
+ @query = query
11
+ end
12
+
13
+ def generate(debug: false)
14
+ # TODO: unfortunately, I think we can only control the select clauses for the top model
15
+ # (as I'm not sure ActiveRecord supports expressing it in the join...)
16
+ @query = add_select(query: query, selector_node: selector)
17
+ eager_hash = _eager(selector)
18
+
19
+ @query = @query.includes(eager_hash)
20
+ explain_query(query, eager_hash) if debug
21
+
22
+ @query
23
+ end
24
+
25
+ def add_select(query:, selector_node:)
26
+ # We're gonna always require the PK of the model, as it is a special case for AR, and the app itself
27
+ # might assume it is always there and not be surprised by the fact that if it isn't, it won't blow up
28
+ # in the same way as any other attribute not being loaded...i.e., ActiveModel::MissingAttributeError: missing attribute: xyz
29
+ select_fields = selector_node.select + [selector_node.resource.model.primary_key.to_sym]
30
+ select_fields.empty? ? query : query.select(*select_fields)
31
+ end
32
+
33
+ def _eager(selector_node)
34
+ selector_node.tracks.each_with_object({}) do |(track_name, track_node), h|
35
+ h[track_name] = _eager(track_node)
36
+ end
37
+ end
38
+
39
+ def explain_query(query, eager_hash)
40
+ prev = ActiveRecord::Base.logger
41
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
42
+ ActiveRecord::Base.logger.debug("Query plan for ...#{selector.resource.model} with selectors: #{JSON.generate(selector.dump)}")
43
+ ActiveRecord::Base.logger.debug(" ActiveRecord query: #{selector.resource.model}.includes(#{eager_hash})")
44
+ query.explain
45
+ ActiveRecord::Base.logger.debug("Query plan end")
46
+ ActiveRecord::Base.logger = prev
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sequel'
4
+
5
+ module Praxis
6
+ module Extensions
7
+ module FieldSelection
8
+ class SequelQuerySelector
9
+ attr_reader :selector, :query
10
+ # Gets a dataset, a selector...and should return a dataset with the selector definition applied.
11
+ def initialize(query:, selectors:)
12
+ @selector = selectors
13
+ @query = query
14
+ end
15
+
16
+ def generate(debug: false)
17
+ @query = add_select(query: query, selector_node: @selector)
18
+
19
+ @query = @selector.tracks.inject(@query) do |ds, (track_name, track_node)|
20
+ ds.eager(track_name => _eager(track_node) )
21
+ end
22
+
23
+ explain_query(query) if debug
24
+ @query
25
+ end
26
+
27
+ def _eager(selector_node)
28
+ lambda do |dset|
29
+ dset = add_select(query: dset, selector_node: selector_node)
30
+
31
+ dset = selector_node.tracks.inject(dset) do |ds, (track_name, track_node)|
32
+ ds.eager(track_name => _eager(track_node) )
33
+ end
34
+
35
+ end
36
+ end
37
+
38
+ def add_select(query:, selector_node:)
39
+ # We're gonna always require the PK of the model, as it is a special case for Sequel, and the app itself
40
+ # might assume it is always there and not be surprised by the fact that if it isn't, it won't blow up
41
+ # in the same way as any other attribute not being loaded...i.e., NoMethodError: undefined method `foobar' for #<...>
42
+ select_fields = selector_node.select + [selector_node.resource.model.primary_key.to_sym]
43
+
44
+ table_name = selector_node.resource.model.table_name
45
+ qualified = select_fields.map { |f| Sequel.qualify(table_name, f) }
46
+ query.select(*qualified)
47
+ end
48
+
49
+ def explain_query(ds)
50
+ prev_loggers = Sequel::Model.db.loggers
51
+ stdout_logger = Logger.new($stdout)
52
+ Sequel::Model.db.loggers = [stdout_logger]
53
+ stdout_logger.debug("Query plan for ...#{selector.resource.model} with selectors: #{JSON.generate(selector.dump)}")
54
+ ds.all
55
+ stdout_logger.debug("Query plan end")
56
+ Sequel::Model.db.loggers = prev_loggers
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,2 @@
1
+ require 'praxis/extensions/rails_compat/request_methods'
2
+ require 'praxis/plugins/rails_plugin'
@@ -0,0 +1,19 @@
1
+ # Make Praxis' request derive from ActionDispatch
2
+ if defined? Praxis::Request
3
+ puts "IT seems that we're trying to redefine Praxis' request parent too late."
4
+ puts "-> try to include the Rails compat pieces earlier in the bootstrap process (before Praxis::Request is requried)"
5
+ exit(-1)
6
+ end
7
+
8
+ begin
9
+ require 'praxis/request_superclassing'
10
+
11
+ module Praxis
12
+ require 'action_dispatch'
13
+ Praxis.request_superclass = ::ActionDispatch::Request
14
+ end
15
+ require 'praxis/request'
16
+ end
17
+
18
+
19
+
@@ -60,7 +60,7 @@ module Praxis
60
60
  when "symbol"
61
61
  return node.content.to_sym
62
62
  when "decimal"
63
- return BigDecimal.new(node.content)
63
+ return BigDecimal(node.content)
64
64
  when "float"
65
65
  return Float(node.content)
66
66
  when "boolean"
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ require 'praxis/extensions/field_selection/active_record_query_selector'
6
+
7
+ module Praxis
8
+ module Mapper
9
+ module ActiveModelCompat
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ attr_accessor :_resource
14
+ end
15
+
16
+ module ClassMethods
17
+ def _filter_query_builder_class
18
+ Praxis::Extensions::ActiveRecordFilterQueryBuilder
19
+ end
20
+
21
+ def _field_selector_query_builder_class
22
+ Praxis::Extensions::FieldSelection::ActiveRecordQuerySelector
23
+ end
24
+
25
+ def _praxis_associations
26
+ orig = self.reflections.clone
27
+
28
+ orig.each_with_object({}) do |(k, v), hash|
29
+ # Assume an 'id' primary key if the system is initializing without AR connected
30
+ # (or without the tables created). This probably means that it's a rake task initializing or so...
31
+ pkey = \
32
+ if v.klass.connected? && v.klass.table_exists?
33
+ v.klass.primary_key
34
+ else
35
+ 'id'
36
+ end
37
+ info = { model: v.klass, primary_key: pkey }
38
+ info[:type] = \
39
+ case v
40
+ when ActiveRecord::Reflection::BelongsToReflection
41
+ :many_to_one
42
+ when ActiveRecord::Reflection::HasManyReflection, ActiveRecord::Reflection::HasOneReflection
43
+ :one_to_many
44
+ when ActiveRecord::Reflection::ThroughReflection
45
+ :many_to_many
46
+ else
47
+ raise "Unknown association type: #{v.class.name} on #{v.klass.name} for #{v.name}"
48
+ end
49
+ # Call out any local (i.e., of this model) columns that participate in the association
50
+ info[:local_key_columns] = local_columns_used_for_the_association(info[:type], v)
51
+ info[:remote_key_columns] = remote_columns_used_for_the_association(info[:type], v)
52
+
53
+ if v.is_a?(ActiveRecord::Reflection::ThroughReflection)
54
+ info[:through] = v.through_reflection.name # TODO: is this correct?
55
+ end
56
+ hash[k.to_sym] = info
57
+ end
58
+ end
59
+
60
+ private
61
+ def local_columns_used_for_the_association(type, assoc_reflection)
62
+ case type
63
+ when :one_to_many
64
+ # The associated table will point to us by key (usually the PK, but not always)
65
+ [assoc_reflection.join_keys.foreign_key.to_sym]
66
+ when :many_to_one
67
+ # We have the FKs to the associated model
68
+ [assoc_reflection.join_keys.foreign_key.to_sym]
69
+ when :many_to_many
70
+ ref = resolve_closest_through_reflection(assoc_reflection)
71
+ # The associated middle table will point to us by key (usually the PK, but not always)
72
+ [ref.join_keys.foreign_key.to_sym] # The foreign key that the last through table points to
73
+ else
74
+ raise "association type #{type} not supported"
75
+ end
76
+ end
77
+
78
+ def remote_columns_used_for_the_association(type, assoc_reflection)
79
+ # It seems that since the reflection is the target of the association, using the join_keys.key
80
+ # will always get us the right column
81
+ case type
82
+ when :one_to_many, :many_to_one, :many_to_many
83
+ [assoc_reflection.join_keys.key.to_sym]
84
+ else
85
+ raise "association type #{type} not supported"
86
+ end
87
+ end
88
+
89
+ # Keep following the association reflections as long as there are middle ones (i.e., through)
90
+ # until we come to the one next to the source
91
+ def resolve_closest_through_reflection(ref)
92
+ return ref unless ref.through_reflection?
93
+ resolve_closest_through_reflection( ref.through_reflection )
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,242 @@
1
+ # 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
+ # 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
7
+
8
+ attr_accessor :record
9
+
10
+ @properties = {}
11
+
12
+ class << self
13
+ attr_reader :model_map
14
+ attr_reader :properties
15
+ end
16
+
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
31
+
32
+ @properties = self.superclass.properties.clone
33
+ end
34
+
35
+ end
36
+
37
+ #TODO: Take symbol/string and resolve the klass (but lazily, so we don't care about load order)
38
+ def self.model(klass=nil)
39
+ if klass
40
+ raise "Model #{klass.name} must be compatible with Praxis. Use ActiveModelCompat or similar compatability plugin." unless klass.methods.include?(:_praxis_associations)
41
+ @model = klass
42
+ self.model_map[klass] = self
43
+ else
44
+ @model
45
+ end
46
+ end
47
+
48
+ def self.property(name, **options)
49
+ self.properties[name] = options
50
+ end
51
+
52
+ def self._finalize!
53
+ finalize_resource_delegates
54
+ define_model_accessors
55
+
56
+ super
57
+ end
58
+
59
+ def self.finalize_resource_delegates
60
+ return unless @resource_delegates
61
+
62
+ @resource_delegates.each do |record_name, record_attributes|
63
+ record_attributes.each do |record_attribute|
64
+ self.define_resource_delegate(record_name, record_attribute)
65
+ end
66
+ end
67
+ end
68
+
69
+
70
+ def self.define_model_accessors
71
+ return if model.nil?
72
+
73
+ model._praxis_associations.each do |k,v|
74
+ unless self.instance_methods.include? k
75
+ define_model_association_accessor(k,v)
76
+ end
77
+ end
78
+ end
79
+
80
+ def self.for_record(record)
81
+ return record._resource if record._resource
82
+
83
+ if resource_class_for_record = model_map[record.class]
84
+ return record._resource = resource_class_for_record.new(record)
85
+ else
86
+ version = self.name.split("::")[0..-2].join("::")
87
+ resource_name = record.class.name.split("::").last
88
+
89
+ raise "No resource class corresponding to the model class '#{record.class}' is defined. (Did you forget to define '#{version}::#{resource_name}'?)"
90
+ end
91
+ end
92
+
93
+
94
+ def self.wrap(records)
95
+ if records.nil?
96
+ return []
97
+ elsif( records.is_a?(Enumerable) )
98
+ return records.compact.map { |record| self.for_record(record) }
99
+ elsif ( records.respond_to?(:to_a) )
100
+ return records.to_a.compact.map { |record| self.for_record(record) }
101
+ else
102
+ return self.for_record(records)
103
+ end
104
+ end
105
+
106
+
107
+ def self.get(condition)
108
+ record = self.model.get(condition)
109
+
110
+ self.wrap(record)
111
+ end
112
+
113
+ def self.all(condition={})
114
+ records = self.model.all(condition)
115
+
116
+ self.wrap(records)
117
+ end
118
+
119
+
120
+ def self.resource_delegates
121
+ @resource_delegates ||= {}
122
+ end
123
+
124
+ def self.resource_delegate(spec)
125
+ spec.each do |resource_name, attributes|
126
+ resource_delegates[resource_name] = attributes
127
+ end
128
+ end
129
+
130
+ # Defines wrappers for model associations that return Resources
131
+ def self.define_model_association_accessor(name, association_spec)
132
+ association_model = association_spec.fetch(:model)
133
+ association_resource_class = model_map[association_model]
134
+
135
+ if association_resource_class
136
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
137
+ def #{name}
138
+ records = record.#{name}
139
+ return nil if records.nil?
140
+ @__#{name} ||= #{association_resource_class}.wrap(records)
141
+ end
142
+ RUBY
143
+ end
144
+ end
145
+
146
+ def self.define_resource_delegate(resource_name, resource_attribute)
147
+ related_model = model._praxis_associations[resource_name][:model]
148
+ related_association = related_model._praxis_associations[resource_attribute]
149
+
150
+ if related_association
151
+ self.define_delegation_for_related_association(resource_name, resource_attribute, related_association)
152
+ else
153
+ self.define_delegation_for_related_attribute(resource_name, resource_attribute)
154
+ end
155
+ end
156
+
157
+
158
+ def self.define_delegation_for_related_attribute(resource_name, resource_attribute)
159
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
160
+ def #{resource_attribute}
161
+ @__#{resource_attribute} ||= if (rec = self.#{resource_name})
162
+ rec.#{resource_attribute}
163
+ end
164
+ end
165
+ RUBY
166
+ end
167
+
168
+ def self.define_delegation_for_related_association(resource_name, resource_attribute, related_association)
169
+ related_resource_class = model_map[related_association[:model]]
170
+ return unless related_resource_class
171
+
172
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
173
+ def #{resource_attribute}
174
+ @__#{resource_attribute} ||= if (rec = self.#{resource_name})
175
+ if (related = rec.#{resource_attribute})
176
+ #{related_resource_class.name}.wrap(related)
177
+ end
178
+ end
179
+ end
180
+ RUBY
181
+ end
182
+
183
+ def self.define_accessor(name)
184
+ if name.to_s =~ /\?/
185
+ ivar_name = "is_#{name.to_s[0..-2]}"
186
+ else
187
+ ivar_name = "#{name}"
188
+ end
189
+
190
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
191
+ def #{name}
192
+ return @__#{ivar_name} if defined? @__#{ivar_name}
193
+ @__#{ivar_name} = record.#{name}
194
+ end
195
+ RUBY
196
+ end
197
+
198
+ # TODO: this shouldn't be needed if we incorporate it with the properties of the mapper...
199
+ def self.filters_mapping(hash)
200
+ @_filter_query_builder_class = model._filter_query_builder_class.for(**hash)
201
+ end
202
+
203
+ def self._filter_query_builder_class
204
+ @_filter_query_builder_class
205
+ end
206
+
207
+ def self.craft_filter_query(base_query, filters:) # rubocop:disable Metrics/AbcSize
208
+ if filters && _filter_query_builder_class
209
+ base_query = _filter_query_builder_class.new(query: base_query, model: model).build_clause(filters)
210
+ end
211
+
212
+ base_query
213
+ end
214
+
215
+ def self.craft_field_selection_query(base_query, selectors:) # rubocop:disable Metrics/AbcSize
216
+ if selectors && model._field_selector_query_builder_class
217
+ debug = Praxis::Application.instance.config.mapper.debug_queries
218
+ base_query = model._field_selector_query_builder_class.new(query: base_query, selectors: selectors).generate(debug: debug)
219
+ end
220
+
221
+ base_query
222
+ end
223
+
224
+ def initialize(record)
225
+ @record = record
226
+ end
227
+
228
+ def respond_to_missing?(name,*)
229
+ @record.respond_to?(name) || super
230
+ end
231
+
232
+ def method_missing(name,*args)
233
+ if @record.respond_to?(name)
234
+ self.class.define_accessor(name)
235
+ self.send(name)
236
+ else
237
+ super
238
+ end
239
+ end
240
+
241
+ end
242
+ end