quiver 0.21.0
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 +7 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +6 -0
- data/README.md +22 -0
- data/Rakefile +17 -0
- data/bin/quiver +9 -0
- data/lib/quiver.rb +46 -0
- data/lib/quiver/abstract_action.rb +31 -0
- data/lib/quiver/action.rb +208 -0
- data/lib/quiver/action/filter_error.rb +33 -0
- data/lib/quiver/action/filter_value.rb +152 -0
- data/lib/quiver/action/invalid_request_body_error.rb +30 -0
- data/lib/quiver/action/pagination_link_builder.rb +67 -0
- data/lib/quiver/adapter.rb +51 -0
- data/lib/quiver/adapter/active_record_adapter_filter.rb +102 -0
- data/lib/quiver/adapter/active_record_helpers.rb +258 -0
- data/lib/quiver/adapter/arec_low_level_creator.rb +82 -0
- data/lib/quiver/adapter/arec_low_level_deleter.rb +74 -0
- data/lib/quiver/adapter/arec_low_level_updater.rb +105 -0
- data/lib/quiver/adapter/filter_helpers.rb +58 -0
- data/lib/quiver/adapter/helpers_helpers.rb +53 -0
- data/lib/quiver/adapter/memory_adapter_filter.rb +71 -0
- data/lib/quiver/adapter/memory_adapter_store.rb +34 -0
- data/lib/quiver/adapter/memory_helpers.rb +182 -0
- data/lib/quiver/adapter/memory_uuid_primary_key.rb +25 -0
- data/lib/quiver/application.rb +128 -0
- data/lib/quiver/cli/app.rb +25 -0
- data/lib/quiver/cli/generators/endpoint.rb +17 -0
- data/lib/quiver/cli/generators/new_application.rb +166 -0
- data/lib/quiver/cli/generators/new_application_cli.rb +25 -0
- data/lib/quiver/cli/server.rb +37 -0
- data/lib/quiver/cli/templates/Gemfile.tt +8 -0
- data/lib/quiver/cli/templates/Rakefile.tt +12 -0
- data/lib/quiver/cli/templates/config.tt +3 -0
- data/lib/quiver/cli/templates/config/database.tt +21 -0
- data/lib/quiver/cli/templates/gemspec.tt +33 -0
- data/lib/quiver/cli/templates/gitignore.tt +10 -0
- data/lib/quiver/cli/templates/lib/application.tt +14 -0
- data/lib/quiver/cli/templates/lib/application/config/router.tt +11 -0
- data/lib/quiver/cli/templates/lib/application/version.tt +3 -0
- data/lib/quiver/cli/templates/spec/spec_helper.tt +19 -0
- data/lib/quiver/duty.rb +34 -0
- data/lib/quiver/duty_master.rb +23 -0
- data/lib/quiver/duty_master/delayed_job_adapter.rb +15 -0
- data/lib/quiver/duty_master/memory_adapter.rb +18 -0
- data/lib/quiver/duty_test_helper.rb +23 -0
- data/lib/quiver/duty_test_helper/delayed_job_helper.rb +9 -0
- data/lib/quiver/duty_test_helper/memory_helper.rb +15 -0
- data/lib/quiver/error.rb +24 -0
- data/lib/quiver/error_collection.rb +60 -0
- data/lib/quiver/json_parser.rb +17 -0
- data/lib/quiver/logger.rb +26 -0
- data/lib/quiver/mapper.rb +311 -0
- data/lib/quiver/mapper/hook.rb +21 -0
- data/lib/quiver/mapper/mapper_result.rb +7 -0
- data/lib/quiver/mapper/not_found_error.rb +27 -0
- data/lib/quiver/mapper/simple_query_builder.rb +70 -0
- data/lib/quiver/mapper/soft_delete.rb +15 -0
- data/lib/quiver/mappers.rb +75 -0
- data/lib/quiver/middleware_stack.rb +35 -0
- data/lib/quiver/model.rb +63 -0
- data/lib/quiver/model/soft_delete.rb +14 -0
- data/lib/quiver/model/validation_error.rb +27 -0
- data/lib/quiver/model/validations.rb +45 -0
- data/lib/quiver/patcher.rb +94 -0
- data/lib/quiver/result.rb +44 -0
- data/lib/quiver/route_helper.rb +16 -0
- data/lib/quiver/router.rb +37 -0
- data/lib/quiver/serialization.rb +6 -0
- data/lib/quiver/serialization/json_api.rb +7 -0
- data/lib/quiver/serialization/json_api/item_type_handler.rb +96 -0
- data/lib/quiver/serialization/json_api/serializer.rb +77 -0
- data/lib/quiver/tasks.rb +31 -0
- data/lib/quiver/validator.rb +83 -0
- data/lib/quiver/validators/base.rb +21 -0
- data/lib/quiver/validators/presence.rb +34 -0
- data/lib/quiver/validators/unique.rb +33 -0
- data/lib/quiver/version.rb +3 -0
- data/quiver.gemspec +42 -0
- metadata +393 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
module Quiver::Action
|
2
|
+
class InvalidRequestBodyError < Quiver::Error
|
3
|
+
def initialize
|
4
|
+
end
|
5
|
+
|
6
|
+
def title
|
7
|
+
'invalid_request_body'
|
8
|
+
end
|
9
|
+
|
10
|
+
def detail
|
11
|
+
'request body must be valid json'
|
12
|
+
end
|
13
|
+
|
14
|
+
def path
|
15
|
+
"/"
|
16
|
+
end
|
17
|
+
|
18
|
+
def status
|
19
|
+
400
|
20
|
+
end
|
21
|
+
|
22
|
+
def code
|
23
|
+
:invalid_request_body
|
24
|
+
end
|
25
|
+
|
26
|
+
def serialization_type
|
27
|
+
'Error'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Quiver
|
2
|
+
module Action
|
3
|
+
class PaginationLinkBuilder
|
4
|
+
def initialize(request_path_with_query, offset, limit, total_count)
|
5
|
+
self.request_path_with_query = request_path_with_query
|
6
|
+
self.offset = offset
|
7
|
+
self.limit = limit
|
8
|
+
self.total_count = total_count
|
9
|
+
end
|
10
|
+
|
11
|
+
def pagination_links
|
12
|
+
links = {}
|
13
|
+
links[:self] = request_path_with_query
|
14
|
+
links[:last] = build_link(last_page)
|
15
|
+
|
16
|
+
links[:next] = build_link(next_page) if next_page
|
17
|
+
links[:prev] = build_link(previous_page) if previous_page
|
18
|
+
|
19
|
+
links
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
attr_accessor :request_path_with_query, :offset, :limit, :total_count
|
25
|
+
|
26
|
+
def build_link(offset)
|
27
|
+
uri = parsed_uri
|
28
|
+
query_params = Rack::Utils.parse_nested_query(uri.query)
|
29
|
+
query_params['page'] ||= {}
|
30
|
+
query_params['page']['limit'] = limit
|
31
|
+
query_params['page']['offset'] = offset
|
32
|
+
uri.query = Rack::Utils.build_nested_query(query_params)
|
33
|
+
uri.to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
def parsed_uri
|
37
|
+
URI.parse(request_path_with_query)
|
38
|
+
end
|
39
|
+
|
40
|
+
def next_page
|
41
|
+
if limit != -1 && total_count > offset + limit
|
42
|
+
offset + limit
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def previous_page
|
47
|
+
if offset > 0
|
48
|
+
if limit == -1
|
49
|
+
0
|
50
|
+
else
|
51
|
+
[offset - limit, 0].max
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def last_page
|
57
|
+
if limit == -1
|
58
|
+
0
|
59
|
+
else
|
60
|
+
# rounds total_count down to the offset that
|
61
|
+
# represents the last page of the resources
|
62
|
+
(total_count / limit) * limit
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Quiver
|
2
|
+
module Adapter
|
3
|
+
class AdapterResult
|
4
|
+
include Quiver::Result
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.included(host)
|
8
|
+
host.send(:extend, ClassMethods)
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def adapter_type(val=nil)
|
13
|
+
if val
|
14
|
+
@adapter_type = val
|
15
|
+
end
|
16
|
+
|
17
|
+
@adapter_type
|
18
|
+
end
|
19
|
+
|
20
|
+
def primary_key_name(val=nil)
|
21
|
+
if val
|
22
|
+
@primary_key_name = val
|
23
|
+
end
|
24
|
+
|
25
|
+
if @primary_key_name.nil?
|
26
|
+
raise RuntimeError, 'mapper adapters must specify primary_key_name'
|
27
|
+
end
|
28
|
+
|
29
|
+
@primary_key_name
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def adapter_type
|
34
|
+
self.class.adapter_type
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
attr_accessor :mapper_klass
|
40
|
+
|
41
|
+
def primary_key_name
|
42
|
+
self.class.primary_key_name
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
require 'quiver/adapter/filter_helpers'
|
48
|
+
require 'quiver/adapter/helpers_helpers'
|
49
|
+
require 'quiver/adapter/memory_helpers'
|
50
|
+
require 'quiver/adapter/memory_uuid_primary_key'
|
51
|
+
require 'quiver/adapter/active_record_helpers'
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Quiver
|
2
|
+
module Adapter
|
3
|
+
module ActiveRecordAdapterFilterDefaults
|
4
|
+
include FilterHelpers
|
5
|
+
|
6
|
+
def self.included(host)
|
7
|
+
host.extend(ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def find(attr_name, opts)
|
12
|
+
raise ArgumentError, ':on must be a table name specified as a symbol' unless opts[:on].is_a?(Symbol)
|
13
|
+
# in the future we want to support arrays, but right now we don't need
|
14
|
+
# it so it is hard to justify the extra time it would take
|
15
|
+
#
|
16
|
+
# tables = opts[:on].is_a?(Array) ? opts[:on] : [opts[:on]]
|
17
|
+
attr_lookup_table[attr_name.to_sym] = opts[:on]
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def attr_lookup_table
|
23
|
+
@attr_lookup_table ||= {}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def equal_filter(memo, attr, value)
|
28
|
+
memo.where(hash_style_attr(attr, value))
|
29
|
+
end
|
30
|
+
|
31
|
+
def not_equal_filter(memo, attr, value)
|
32
|
+
memo.where.not(hash_style_attr(attr, value))
|
33
|
+
end
|
34
|
+
|
35
|
+
def in_filter(memo, attr, value)
|
36
|
+
memo.where(hash_style_attr(attr, value))
|
37
|
+
end
|
38
|
+
|
39
|
+
def not_in_filter(memo, attr, value)
|
40
|
+
memo.where.not(hash_style_attr(attr, value))
|
41
|
+
end
|
42
|
+
|
43
|
+
def less_than_filter(memo, attr, value)
|
44
|
+
memo.where("#{string_style_attr(attr)} < ?", value)
|
45
|
+
end
|
46
|
+
|
47
|
+
def greater_than_filter(memo, attr, value)
|
48
|
+
memo.where("#{string_style_attr(attr)} > ?", value)
|
49
|
+
end
|
50
|
+
|
51
|
+
def less_than_or_equal_filter(memo, attr, value)
|
52
|
+
memo.where("#{string_style_attr(attr)} <= ?", value)
|
53
|
+
end
|
54
|
+
|
55
|
+
def greater_than_or_equal_filter(memo, attr, value)
|
56
|
+
memo.where("#{string_style_attr(attr)} >= ?", value)
|
57
|
+
end
|
58
|
+
|
59
|
+
def nil_filter(memo, attr, value)
|
60
|
+
if value == 'true' || value == true
|
61
|
+
memo.where("#{string_style_attr(attr)} IS NULL")
|
62
|
+
else
|
63
|
+
memo.where("#{string_style_attr(attr)} IS NOT NULL")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def not_nil_filter(memo, attr, value)
|
68
|
+
if value == 'true' || value == true
|
69
|
+
memo.where("#{string_style_attr(attr)} IS NOT NULL")
|
70
|
+
else
|
71
|
+
memo.where("#{string_style_attr(attr)} IS NULL")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def attr_lookup(attr)
|
78
|
+
self.class.send(:attr_lookup_table)[attr]
|
79
|
+
end
|
80
|
+
|
81
|
+
def hash_style_attr(attr, value)
|
82
|
+
if table = attr_lookup(attr)
|
83
|
+
{table => {attr => value}}
|
84
|
+
else
|
85
|
+
{attr => value}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def string_style_attr(attr)
|
90
|
+
if table = attr_lookup(attr)
|
91
|
+
"#{table}.#{attr}"
|
92
|
+
else
|
93
|
+
attr
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
class ActiveRecordAdapterFilter
|
99
|
+
include ActiveRecordAdapterFilterDefaults
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,258 @@
|
|
1
|
+
require 'quiver/adapter/active_record_adapter_filter'
|
2
|
+
require 'quiver/adapter/arec_low_level_creator'
|
3
|
+
require 'quiver/adapter/arec_low_level_updater'
|
4
|
+
require 'quiver/adapter/arec_low_level_deleter'
|
5
|
+
require 'quiver/mappers'
|
6
|
+
|
7
|
+
module Quiver
|
8
|
+
module Adapter
|
9
|
+
module ActiveRecord
|
10
|
+
include Quiver::Adapter::HelpersHelpers
|
11
|
+
|
12
|
+
def self.included(host)
|
13
|
+
super
|
14
|
+
|
15
|
+
host.adapter_type(:active_record)
|
16
|
+
host.send(:extend, ClassMethods)
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
def define_record_class(name, options)
|
21
|
+
return if !self.parents[-2]::Application.using_active_record
|
22
|
+
|
23
|
+
table_name = options.fetch(:table)
|
24
|
+
|
25
|
+
record_classes[name] = Class.new(::ActiveRecord::Base) do
|
26
|
+
self.table_name = table_name
|
27
|
+
self.inheritance_column = 'a_name_that_will_never_be_used'
|
28
|
+
|
29
|
+
def self.name=(val)
|
30
|
+
@reported_name = val
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.name
|
34
|
+
@reported_name
|
35
|
+
end
|
36
|
+
end.tap do |klass|
|
37
|
+
klass.name = name
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def record_classes
|
42
|
+
@record_classes ||= {}
|
43
|
+
end
|
44
|
+
|
45
|
+
def use_record_class(val=nil)
|
46
|
+
if val
|
47
|
+
@use_record_class = val
|
48
|
+
end
|
49
|
+
|
50
|
+
@use_record_class
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def find(primary_key)
|
55
|
+
Quiver::Adapter::AdapterResult.new do |errors|
|
56
|
+
if record = base_query.find_by(primary_key_name => primary_key)
|
57
|
+
load_additional([record.attributes.symbolize_keys]).first
|
58
|
+
else
|
59
|
+
errors << Quiver::Mapper::NotFoundError.new('record', 'not_found')
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def count
|
66
|
+
count = default_record_class.count
|
67
|
+
|
68
|
+
Quiver::Adapter::AdapterResult.new(count)
|
69
|
+
end
|
70
|
+
|
71
|
+
def create(attributes, transaction)
|
72
|
+
persister = ARecLowLevelCreator.new(self.class, attributes)
|
73
|
+
|
74
|
+
if attributes[:__type__]
|
75
|
+
attributes = attributes.merge(
|
76
|
+
attributes[:__type__][:name] => attributes[:__type__][:value]
|
77
|
+
)
|
78
|
+
|
79
|
+
attributes.delete(:__type__)
|
80
|
+
end
|
81
|
+
|
82
|
+
begin
|
83
|
+
create_mappings(attributes, persister)
|
84
|
+
rescue ::ActiveRecord::ActiveRecordError
|
85
|
+
transaction.rollback!
|
86
|
+
persister.failed!
|
87
|
+
end
|
88
|
+
|
89
|
+
Quiver::Adapter::AdapterResult.new do |errors|
|
90
|
+
if persister.success?
|
91
|
+
persister.result
|
92
|
+
else
|
93
|
+
errors << Quiver::Error.new('record', 'not_persisted')
|
94
|
+
nil
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def update(attributes, transaction)
|
100
|
+
persister = ARecLowLevelUpdater.new(self.class, attributes)
|
101
|
+
|
102
|
+
if attributes[:__type__]
|
103
|
+
attributes = attributes.merge(
|
104
|
+
attributes[:__type__][:name] => attributes[:__type__][:value]
|
105
|
+
)
|
106
|
+
|
107
|
+
attributes.delete(:__type__)
|
108
|
+
end
|
109
|
+
|
110
|
+
begin
|
111
|
+
update_mappings(attributes, persister)
|
112
|
+
rescue ::ActiveRecord::ActiveRecordError
|
113
|
+
transaction.rollback!
|
114
|
+
persister.failed!
|
115
|
+
end
|
116
|
+
|
117
|
+
Quiver::Adapter::AdapterResult.new do |errors|
|
118
|
+
if persister.success?
|
119
|
+
persister.result
|
120
|
+
else
|
121
|
+
errors << Quiver::Error.new('record', 'not_updated')
|
122
|
+
nil
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def hard_delete(attributes, transaction)
|
128
|
+
unpersister = ARecLowLevelDeleter.new(self.class, attributes)
|
129
|
+
|
130
|
+
if attributes[:__type__]
|
131
|
+
attributes = attributes.merge(
|
132
|
+
attributes[:__type__][:name] => attributes[:__type__][:value]
|
133
|
+
)
|
134
|
+
|
135
|
+
attributes.delete(:__type__)
|
136
|
+
end
|
137
|
+
|
138
|
+
begin
|
139
|
+
delete_mappings(attributes, unpersister)
|
140
|
+
rescue ::ActiveRecord::ActiveRecordError
|
141
|
+
transaction.rollback!
|
142
|
+
unpersister.failed!
|
143
|
+
end
|
144
|
+
|
145
|
+
Quiver::Adapter::AdapterResult.new do |errors|
|
146
|
+
if unpersister.success?
|
147
|
+
{}
|
148
|
+
else
|
149
|
+
errors << Quiver::Error.new('record', 'not_deleted')
|
150
|
+
nil
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def query(q={})
|
156
|
+
filter_params = q[:filter] || {}
|
157
|
+
sort_params = q[:sort] || {}
|
158
|
+
pagination_params = q[:page] || {}
|
159
|
+
|
160
|
+
query = filter_klass.new(
|
161
|
+
base_query,
|
162
|
+
filter_params
|
163
|
+
).filter
|
164
|
+
|
165
|
+
count_query = filter_klass.new(
|
166
|
+
base_count_query,
|
167
|
+
filter_params
|
168
|
+
).filter
|
169
|
+
|
170
|
+
sort_params.each do |attr, asc|
|
171
|
+
order = asc ? 'ASC' : 'DESC'
|
172
|
+
query = query.order("#{attr} #{order}")
|
173
|
+
count_query = count_query.order("#{attr} #{order}")
|
174
|
+
end
|
175
|
+
|
176
|
+
total_count = count_query.count
|
177
|
+
|
178
|
+
if pagination_params['limit'] && pagination_params['limit'] != -1
|
179
|
+
query = query.limit(pagination_params['limit'])
|
180
|
+
end
|
181
|
+
|
182
|
+
if pagination_params['offset']
|
183
|
+
query = query.offset(pagination_params['offset'])
|
184
|
+
end
|
185
|
+
|
186
|
+
objects = load_additional(query.map(&:attributes).map(&:symbolize_keys))
|
187
|
+
result = Quiver::Adapter::AdapterResult.new(objects)
|
188
|
+
|
189
|
+
if pagination_params.any?
|
190
|
+
result.data[:pagination_offset] = pagination_params['offset'] || 0
|
191
|
+
result.data[:pagination_limit] = pagination_params['limit'] || -1
|
192
|
+
result.data[:total_count] = total_count
|
193
|
+
end
|
194
|
+
|
195
|
+
result
|
196
|
+
end
|
197
|
+
|
198
|
+
private
|
199
|
+
|
200
|
+
def default_record_class
|
201
|
+
self.class.record_classes[self.class.use_record_class]
|
202
|
+
end
|
203
|
+
|
204
|
+
def fetch_and_hydrate(record_klass, primary_key)
|
205
|
+
errors = Quiver::ErrorCollection.new
|
206
|
+
|
207
|
+
if record = record_klass.find_by(primary_key_name => primary_key)
|
208
|
+
attributes = load_additional([record.attributes]).first
|
209
|
+
|
210
|
+
object = hydrate(attributes)
|
211
|
+
else
|
212
|
+
errors << Quiver::Mapper::NotFoundError.new('record', 'not_found')
|
213
|
+
object = nil
|
214
|
+
end
|
215
|
+
|
216
|
+
Quiver::Mapper::MapperResult.new(object, errors)
|
217
|
+
end
|
218
|
+
|
219
|
+
def base_query
|
220
|
+
default_record_class.where({})
|
221
|
+
end
|
222
|
+
|
223
|
+
def base_count_query
|
224
|
+
default_record_class.where({})
|
225
|
+
end
|
226
|
+
|
227
|
+
def filter_klass
|
228
|
+
Quiver::Adapter::ActiveRecordAdapterFilter
|
229
|
+
end
|
230
|
+
|
231
|
+
def load_additional(items)
|
232
|
+
items
|
233
|
+
end
|
234
|
+
|
235
|
+
def mappings(attributes, p)
|
236
|
+
p.map(
|
237
|
+
attributes,
|
238
|
+
to: self.class.use_record_class,
|
239
|
+
primary: true
|
240
|
+
)
|
241
|
+
end
|
242
|
+
|
243
|
+
def create_mappings(attributes, p)
|
244
|
+
mappings(attributes, p)
|
245
|
+
end
|
246
|
+
|
247
|
+
def update_mappings(attributes, p)
|
248
|
+
mappings(attributes, p)
|
249
|
+
end
|
250
|
+
|
251
|
+
def delete_mappings(attributes, p)
|
252
|
+
mappings(attributes, p)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
ActiveRecordHelpers = ActiveRecord
|
257
|
+
end
|
258
|
+
end
|