praxis 0.21 → 2.0.pre.3

Sign up to get free protection for your applications and to get access to all the features.
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