praxis 0.22.pre.2 → 2.0.pre.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +323 -324
- data/lib/praxis/action_definition.rb +7 -9
- data/lib/praxis/api_definition.rb +27 -44
- data/lib/praxis/api_general_info.rb +2 -3
- data/lib/praxis/application.rb +14 -141
- data/lib/praxis/bootloader.rb +1 -2
- data/lib/praxis/bootloader_stages/environment.rb +13 -0
- data/lib/praxis/controller.rb +0 -2
- data/lib/praxis/dispatcher.rb +4 -6
- data/lib/praxis/docs/generator.rb +8 -18
- data/lib/praxis/docs/link_builder.rb +1 -1
- data/lib/praxis/error_handler.rb +5 -5
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +1 -1
- data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +125 -0
- data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +16 -18
- data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +5 -5
- data/lib/praxis/extensions/field_selection.rb +1 -12
- data/lib/praxis/extensions/rendering.rb +1 -1
- data/lib/praxis/file_group.rb +1 -1
- data/lib/praxis/handlers/xml.rb +1 -1
- data/lib/praxis/mapper/active_model_compat.rb +63 -0
- data/lib/praxis/mapper/resource.rb +242 -0
- data/lib/praxis/mapper/selector_generator.rb +126 -0
- data/lib/praxis/mapper/sequel_compat.rb +37 -0
- data/lib/praxis/middleware_app.rb +13 -15
- data/lib/praxis/multipart/part.rb +3 -5
- data/lib/praxis/plugins/mapper_plugin.rb +50 -0
- data/lib/praxis/request.rb +14 -7
- data/lib/praxis/request_stages/response.rb +2 -3
- data/lib/praxis/resource_definition.rb +10 -14
- data/lib/praxis/response.rb +6 -5
- data/lib/praxis/response_definition.rb +5 -7
- data/lib/praxis/response_template.rb +3 -4
- data/lib/praxis/responses/http.rb +36 -0
- data/lib/praxis/responses/internal_server_error.rb +12 -3
- data/lib/praxis/responses/multipart_ok.rb +11 -4
- data/lib/praxis/responses/validation_error.rb +10 -1
- data/lib/praxis/router.rb +3 -3
- data/lib/praxis/tasks/api_docs.rb +2 -10
- data/lib/praxis/tasks/routes.rb +0 -1
- data/lib/praxis/version.rb +1 -1
- data/lib/praxis.rb +13 -9
- data/praxis.gemspec +2 -3
- data/spec/functional_spec.rb +0 -1
- data/spec/praxis/action_definition_spec.rb +15 -26
- data/spec/praxis/api_definition_spec.rb +8 -13
- data/spec/praxis/api_general_info_spec.rb +8 -3
- data/spec/praxis/application_spec.rb +7 -13
- data/spec/praxis/handlers/xml_spec.rb +2 -2
- data/spec/praxis/mapper/resource_spec.rb +169 -0
- data/spec/praxis/mapper/selector_generator_spec.rb +301 -0
- data/spec/praxis/middleware_app_spec.rb +15 -9
- data/spec/praxis/request_spec.rb +7 -17
- data/spec/praxis/request_stages/validate_spec.rb +1 -1
- data/spec/praxis/resource_definition_spec.rb +10 -12
- data/spec/praxis/response_definition_spec.rb +5 -22
- data/spec/praxis/response_spec.rb +5 -12
- data/spec/praxis/responses/internal_server_error_spec.rb +5 -2
- data/spec/praxis/router_spec.rb +4 -8
- data/spec/spec_app/app/models/person.rb +3 -3
- data/spec/spec_app/config/environment.rb +3 -21
- data/spec/spec_app/config.ru +6 -1
- data/spec/spec_helper.rb +2 -17
- data/spec/support/spec_resources.rb +131 -0
- metadata +19 -31
- data/lib/praxis/extensions/attribute_filtering/query_builder.rb +0 -39
- data/lib/praxis/extensions/attribute_filtering.rb +0 -28
- data/lib/praxis/extensions/mapper_selectors.rb +0 -16
- data/lib/praxis/media_type_collection.rb +0 -127
- data/lib/praxis/plugins/praxis_mapper_plugin.rb +0 -246
- data/spec/praxis/media_type_collection_spec.rb +0 -157
- data/spec/praxis/plugins/praxis_mapper_plugin_spec.rb +0 -142
@@ -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
|
@@ -3,49 +3,47 @@ module Praxis
|
|
3
3
|
module Extensions
|
4
4
|
module FieldSelection
|
5
5
|
class ActiveRecordQuerySelector
|
6
|
-
attr_reader :selector, :
|
6
|
+
attr_reader :selector, :query, :top_model, :resolved, :root
|
7
7
|
# Gets a dataset, a selector...and should return a dataset with the selector definition applied.
|
8
|
-
def initialize(
|
8
|
+
def initialize(query:, model:, selectors:, resolved:)
|
9
9
|
@selector = selectors
|
10
|
-
@
|
10
|
+
@query = query
|
11
11
|
@top_model = model
|
12
12
|
@resolved = resolved
|
13
13
|
@seen = Set.new
|
14
14
|
@root = model.table_name
|
15
15
|
end
|
16
16
|
|
17
|
-
def add_select(
|
17
|
+
def add_select(query:, model:, table_name:)
|
18
18
|
if (fields = fields_for(model))
|
19
19
|
# Note, let's always add the pk fields so that associations can load properly
|
20
20
|
fields = fields | [model.primary_key.to_sym]
|
21
|
-
|
21
|
+
query.select(*fields)
|
22
22
|
else
|
23
|
-
|
23
|
+
query
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
27
|
def generate
|
28
28
|
# TODO: unfortunately, I think we can only control the select clauses for the top model
|
29
29
|
# (as I'm not sure ActiveRecord supports expressing it in the join...)
|
30
|
-
@
|
30
|
+
@query = add_select(query: query, model: top_model, table_name: root)
|
31
31
|
|
32
|
-
@
|
32
|
+
@query.includes(_eager(top_model, resolved) )
|
33
33
|
end
|
34
34
|
|
35
35
|
def _eager(model, resolved)
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
dataset << { track => _eager(assoc_model, resolved[track]) }
|
44
|
-
end
|
36
|
+
tracks = only_assoc_for(model, resolved)
|
37
|
+
tracks.inject([]) do |dataset, track|
|
38
|
+
next dataset if @seen.include?([model, track])
|
39
|
+
@seen << [model, track]
|
40
|
+
assoc_model = model._praxis_associations[track][:model]
|
41
|
+
dataset << { track => _eager(assoc_model, resolved[track]) }
|
42
|
+
end
|
45
43
|
end
|
46
44
|
|
47
45
|
def only_assoc_for(model, hash)
|
48
|
-
hash.keys.reject { |assoc| model.
|
46
|
+
hash.keys.reject { |assoc| model._praxis_associations[assoc].nil? }
|
49
47
|
end
|
50
48
|
|
51
49
|
def fields_for(model)
|
@@ -5,9 +5,9 @@ module Praxis
|
|
5
5
|
class SequelQuerySelector
|
6
6
|
attr_reader :selector, :ds, :top_model, :resolved, :root
|
7
7
|
# Gets a dataset, a selector...and should return a dataset with the selector definition applied.
|
8
|
-
def initialize(
|
8
|
+
def initialize(query:, model:, selectors:, resolved:)
|
9
9
|
@selector = selectors
|
10
|
-
@ds =
|
10
|
+
@ds = query
|
11
11
|
@top_model = model
|
12
12
|
@resolved = resolved
|
13
13
|
@seen = Set.new
|
@@ -32,7 +32,7 @@ module Praxis
|
|
32
32
|
@ds = tracks.inject(@ds) do |dataset, track|
|
33
33
|
next dataset if @seen.include?([top_model, track])
|
34
34
|
@seen << [top_model, track]
|
35
|
-
assoc_model = top_model.
|
35
|
+
assoc_model = top_model._praxis_associations[track][:model]
|
36
36
|
# hash[track] = _eager(assoc_model, resolved[track])
|
37
37
|
dataset.eager(track => _eager(assoc_model, resolved[track]))
|
38
38
|
end
|
@@ -46,14 +46,14 @@ module Praxis
|
|
46
46
|
tracks.inject(d) do |dataset, track|
|
47
47
|
next dataset if @seen.include?([model, track])
|
48
48
|
@seen << [model, track]
|
49
|
-
assoc_model = model.
|
49
|
+
assoc_model = model._praxis_associations[track][:model]
|
50
50
|
dataset.eager(track => _eager(assoc_model, resolved[track]))
|
51
51
|
end
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
55
55
|
def only_assoc_for(model, hash)
|
56
|
-
hash.keys.reject { |assoc| model.
|
56
|
+
hash.keys.reject { |assoc| model._praxis_associations[assoc].nil? }
|
57
57
|
end
|
58
58
|
|
59
59
|
def fields_for(model)
|
@@ -1,13 +1,2 @@
|
|
1
1
|
require 'attributor/extras/field_selector'
|
2
|
-
|
3
|
-
require 'praxis/extensions/field_selection/field_selector'
|
4
|
-
# TODO: we should conditionally require it based on what ORM/s we want...
|
5
|
-
require 'praxis/extensions/field_selection/active_record_query_selector'
|
6
|
-
|
7
|
-
|
8
|
-
module Praxis
|
9
|
-
module Extensions
|
10
|
-
module FieldSelection
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
2
|
+
require 'praxis/extensions/field_selection/field_selector'
|
@@ -24,7 +24,7 @@ module Praxis
|
|
24
24
|
response.body = render(object, include_nil: include_nil)
|
25
25
|
response
|
26
26
|
rescue Praxis::Renderer::CircularRenderingError => e
|
27
|
-
Praxis::Application.
|
27
|
+
Praxis::Application.instance.validation_handler.handle!(
|
28
28
|
summary: "Circular Rendering Error when rendering response. " +
|
29
29
|
"Please especify a view to narrow the dependent fields, or narrow your field set.",
|
30
30
|
exception: e,
|
data/lib/praxis/file_group.rb
CHANGED
data/lib/praxis/handlers/xml.rb
CHANGED
@@ -0,0 +1,63 @@
|
|
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
|
+
|
50
|
+
if v.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
51
|
+
info[:through] = v.through_reflection.name # TODO: is this correct?
|
52
|
+
end
|
53
|
+
|
54
|
+
# TODO: add more keys for the association to make true praxis mapper functions happy
|
55
|
+
hash[k.to_sym] = info
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
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:, resolved:) # rubocop:disable Metrics/AbcSize
|
216
|
+
if selectors && model._field_selector_query_builder_class
|
217
|
+
base_query = model._field_selector_query_builder_class.new(query: base_query, model: self.model,
|
218
|
+
selectors: selectors, resolved: resolved).generate
|
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
|