praxis 0.22.pre.2 → 2.0.pre.1
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.
- 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
|