trax_model 0.0.96 → 0.0.97
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +7 -0
- data/Gemfile +1 -0
- data/lib/trax/model.rb +2 -1
- data/lib/trax/model/attributes/definitions.rb +1 -1
- data/lib/trax/model/attributes/types/struct.rb +91 -0
- data/lib/trax/model/struct_extensions.rb +126 -0
- data/lib/trax_model/version.rb +1 -1
- data/spec/spec_helper.rb +1 -0
- data/spec/support/models.rb +4 -2
- data/spec/support/pg/models.rb +13 -7
- data/spec/trax/model/attributes/types/{json_spec.rb → struct_spec.rb} +2 -2
- data/spec/trax/model/attributes_spec.rb +1 -1
- data/spec/trax/model/{struct_spec.rb → struct_extensions_spec.rb} +1 -1
- data/trax_model.gemspec +1 -0
- metadata +23 -8
- data/lib/trax/model/attributes/types/json.rb +0 -229
- data/lib/trax/model/struct.rb +0 -189
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fe39dac9ff74e51159a879a215dc98ccc0bd78c4
|
4
|
+
data.tar.gz: 373428f4903798d45be49fa6d9b539a68c5ed890
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ddd740eab7478af4d5b6b3c02556d281191899d8f5ad6ae1882d937e38120f0c759a2970d32dd58fed65d806928e3fc53a02ccf89efff3f4d1afc8a93fb6dba8
|
7
|
+
data.tar.gz: e71ce067fa72afa23bf4149849fd29d37a4e6eb823e2033d0a96835fbc1592ec3e537cf2f99fca3ba1ddd249ab3a8a9dc6675fe33fd64eb2e7e7252c4561e1c6
|
data/.travis.yml
ADDED
data/Gemfile
CHANGED
data/lib/trax/model.rb
CHANGED
@@ -40,6 +40,7 @@ module Trax
|
|
40
40
|
autoload :Railtie
|
41
41
|
autoload :STI
|
42
42
|
autoload :Struct
|
43
|
+
autoload :StructExtensions
|
43
44
|
autoload :Validators
|
44
45
|
|
45
46
|
include ::Trax::Model::Matchable
|
@@ -159,4 +160,4 @@ module Trax
|
|
159
160
|
end
|
160
161
|
end
|
161
162
|
|
162
|
-
::Trax::Model::Railtie if defined?(Rails)
|
163
|
+
::Trax::Model::Railtie if defined?(::Rails)
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'hashie/extensions/ignore_undeclared'
|
2
|
+
|
3
|
+
module Trax
|
4
|
+
module Model
|
5
|
+
module Attributes
|
6
|
+
module Types
|
7
|
+
class Struct < ::Trax::Model::Attributes::Type
|
8
|
+
def self.define_attribute(klass, attribute_name, **options, &block)
|
9
|
+
klass_name = "#{klass.fields_module.name.underscore}/#{attribute_name}".camelize
|
10
|
+
attribute_klass = if options.key?(:extend)
|
11
|
+
_klass_prototype = options[:extend].constantize
|
12
|
+
_klass = ::Trax::Core::NamedClass.new(klass_name, _klass_prototype, :parent_definition => klass, &block)
|
13
|
+
_klass.include(::Trax::Model::StructExtensions)
|
14
|
+
_klass
|
15
|
+
else
|
16
|
+
::Trax::Core::NamedClass.new(klass_name, Value, :parent_definition => klass, &block)
|
17
|
+
end
|
18
|
+
|
19
|
+
klass.attribute(attribute_name, typecaster_klass.new(target_klass: attribute_klass))
|
20
|
+
klass.validates(attribute_name, :json_attribute => true) unless options.key?(:validate) && !options[:validate]
|
21
|
+
klass.default_value_for(attribute_name) { {} }
|
22
|
+
define_model_accessors(klass, attribute_name, attribute_klass, options[:model_accessors]) if options.key?(:model_accessors) && options[:model_accessors]
|
23
|
+
end
|
24
|
+
|
25
|
+
class Value < ::Trax::Core::Types::Struct
|
26
|
+
include ::Trax::Model::StructExtensions
|
27
|
+
end
|
28
|
+
|
29
|
+
class TypeCaster < ActiveRecord::Type::Value
|
30
|
+
include ::ActiveRecord::Type::Mutable
|
31
|
+
|
32
|
+
def initialize(*args, target_klass:)
|
33
|
+
super(*args)
|
34
|
+
|
35
|
+
@target_klass = target_klass
|
36
|
+
end
|
37
|
+
|
38
|
+
def type
|
39
|
+
:struct
|
40
|
+
end
|
41
|
+
|
42
|
+
def type_cast_from_user(value)
|
43
|
+
value.is_a?(@target_klass) ? value : @target_klass.new(value || {})
|
44
|
+
end
|
45
|
+
|
46
|
+
def type_cast_from_database(value)
|
47
|
+
value.present? ? @target_klass.new(::JSON.parse(value)) : value
|
48
|
+
end
|
49
|
+
|
50
|
+
def type_cast_for_database(value)
|
51
|
+
value.present? ? value.to_serializable_hash.to_json : nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
self.value_klass = ::Trax::Model::Attributes::Types::Struct::Value
|
56
|
+
self.typecaster_klass = ::Trax::Model::Attributes::Types::Struct::TypeCaster
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def self.define_model_accessors(model, attribute_name, struct_attribute, option_value)
|
61
|
+
properties_to_define = if [ true ].include?(option_value)
|
62
|
+
struct_attribute.properties.to_a
|
63
|
+
elsif option_value.is_a?(Hash) && option_value.has_key?(:only)
|
64
|
+
struct_attribute.properties.to_a & option_value[:only]
|
65
|
+
elsif option_value.is_a?(Hash) && option_value.has_key?(:except)
|
66
|
+
struct_attribute.properties.to_a - option_value[:except]
|
67
|
+
elsif option_value.is_a?(Array)
|
68
|
+
struct_attribute.properties.to_a & option_value
|
69
|
+
else
|
70
|
+
raise Trax::Model::Errors::InvalidOption.new(
|
71
|
+
:option => :model_accessors,
|
72
|
+
:valid_choices => ["true", "array of properties", "hash with :only or :except keys"]
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
properties_to_define.each do |_property|
|
77
|
+
getter_method, setter_method = _property.to_sym, :"#{_property}="
|
78
|
+
|
79
|
+
model.__send__(:define_method, setter_method) do |val|
|
80
|
+
self[attribute_name] = {} unless self[attribute_name]
|
81
|
+
self.__send__(attribute_name).__send__(setter_method, val)
|
82
|
+
end
|
83
|
+
|
84
|
+
model.delegate(getter_method, :to => attribute_name)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module Trax
|
2
|
+
module Model
|
3
|
+
module StructExtensions
|
4
|
+
extend ::ActiveSupport::Concern
|
5
|
+
include ::ActiveModel::Validations
|
6
|
+
|
7
|
+
included do
|
8
|
+
attr_reader :record
|
9
|
+
end
|
10
|
+
|
11
|
+
def inspect
|
12
|
+
self.to_hash.inspect
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_json
|
16
|
+
self.to_hash.to_json
|
17
|
+
end
|
18
|
+
|
19
|
+
def value
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
#bit of a hack for the sake of strong params for now
|
25
|
+
def permitted_keys
|
26
|
+
@permitted_keys ||= properties.map(&:to_sym)
|
27
|
+
end
|
28
|
+
|
29
|
+
def define_model_scopes_for(*attribute_names)
|
30
|
+
attribute_names.each do |attribute_name|
|
31
|
+
define_model_scope_for(attribute_name)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def define_model_scope_for(attribute_name, **options)
|
36
|
+
attribute_klass = fields[attribute_name]
|
37
|
+
|
38
|
+
case fields[attribute_name].type
|
39
|
+
when :boolean
|
40
|
+
define_where_scopes_for_boolean_property(attribute_name, attribute_klass, **options)
|
41
|
+
when :enum
|
42
|
+
define_scopes_for_enum(attribute_name, attribute_klass, **options)
|
43
|
+
else
|
44
|
+
define_where_scopes_for_property(attribute_name, attribute_klass, **options)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
#this only supports properties 1 level deep, but works beautifully
|
49
|
+
#I.E. for this structure
|
50
|
+
# define_attributes do
|
51
|
+
# struct :custom_fields do
|
52
|
+
# enum :color, :default => :blue do
|
53
|
+
# define :blue, 1
|
54
|
+
# define :red, 2
|
55
|
+
# define :green, 3
|
56
|
+
# end
|
57
|
+
# end
|
58
|
+
# end
|
59
|
+
# ::Product.by_custom_fields_color(:blue, :red)
|
60
|
+
# will return #{Product color=blue}, #{Product color=red}
|
61
|
+
def define_scopes_for_enum(attribute_name, enum_klass, as:nil)
|
62
|
+
return unless has_active_record_ancestry?(enum_klass)
|
63
|
+
|
64
|
+
model_class = model_class_for_property(enum_klass)
|
65
|
+
field_name = enum_klass.parent_definition.name.demodulize.underscore
|
66
|
+
attribute_name = enum_klass.name.demodulize.underscore
|
67
|
+
scope_name = as || :"by_#{field_name}_#{attribute_name}"
|
68
|
+
model_class.scope(scope_name, lambda{ |*_scope_values|
|
69
|
+
_integer_values = enum_klass.select_values(*_scope_values.flat_compact_uniq!)
|
70
|
+
_integer_values.map!(&:to_s)
|
71
|
+
model_class.where("#{field_name} -> '#{attribute_name}' IN(?)", _integer_values)
|
72
|
+
})
|
73
|
+
end
|
74
|
+
|
75
|
+
def define_where_scopes_for_boolean_property(attribute_name, property_klass, as:nil)
|
76
|
+
return unless has_active_record_ancestry?(property_klass)
|
77
|
+
|
78
|
+
model_class = model_class_for_property(property_klass)
|
79
|
+
field_name = property_klass.parent_definition.name.demodulize.underscore
|
80
|
+
attribute_name = property_klass.name.demodulize.underscore
|
81
|
+
scope_name = as || :"by_#{field_name}_#{attribute_name}"
|
82
|
+
model_class.scope(scope_name, lambda{ |*_scope_values|
|
83
|
+
_scope_values.map!(&:to_s).flat_compact_uniq!
|
84
|
+
model_class.where("#{field_name} -> '#{attribute_name}' IN(?)", _scope_values)
|
85
|
+
})
|
86
|
+
end
|
87
|
+
|
88
|
+
def define_where_scopes_for_property(attribute_name, property_klass, as:nil)
|
89
|
+
return unless has_active_record_ancestry?(property_klass)
|
90
|
+
|
91
|
+
model_class = model_class_for_property(property_klass)
|
92
|
+
field_name = property_klass.parent_definition.name.demodulize.underscore
|
93
|
+
attribute_name = property_klass.name.demodulize.underscore
|
94
|
+
scope_name = as || :"by_#{field_name}_#{attribute_name}"
|
95
|
+
|
96
|
+
model_class.scope(scope_name, lambda{ |*_scope_values|
|
97
|
+
_scope_values.map!(&:to_s).flat_compact_uniq!
|
98
|
+
model_class.where("#{field_name} ->> '#{attribute_name}' IN(?)", _scope_values)
|
99
|
+
})
|
100
|
+
end
|
101
|
+
|
102
|
+
def has_active_record_ancestry?(property_klass)
|
103
|
+
return false unless property_klass.respond_to?(:parent_definition)
|
104
|
+
|
105
|
+
result = if property_klass.parent_definition.ancestors.include?(::ActiveRecord::Base)
|
106
|
+
true
|
107
|
+
else
|
108
|
+
has_active_record_ancestry?(property_klass.parent_definition)
|
109
|
+
end
|
110
|
+
|
111
|
+
result
|
112
|
+
end
|
113
|
+
|
114
|
+
def model_class_for_property(property_klass)
|
115
|
+
result = if property_klass.parent_definition.ancestors.include?(::ActiveRecord::Base)
|
116
|
+
property_klass.parent_definition
|
117
|
+
else
|
118
|
+
model_class_for_property(property_klass.parent_definition)
|
119
|
+
end
|
120
|
+
|
121
|
+
result
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
data/lib/trax_model/version.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
data/spec/support/models.rb
CHANGED
@@ -114,9 +114,11 @@ end
|
|
114
114
|
class SwinglineStaplerAttributeSet < ::ActiveRecord::Base
|
115
115
|
end
|
116
116
|
|
117
|
-
require 'trax/model/struct'
|
118
117
|
|
119
|
-
|
118
|
+
|
119
|
+
class StoreCategory < ::Trax::Core::Types::Struct
|
120
|
+
include ::Trax::Model::StructExtensions
|
121
|
+
|
120
122
|
string :name
|
121
123
|
struct :meta_attributes do
|
122
124
|
string :description
|
data/spec/support/pg/models.rb
CHANGED
@@ -17,6 +17,8 @@ module Ecommerce
|
|
17
17
|
|
18
18
|
define_attributes do
|
19
19
|
struct :specifics, :model_accessors => true, :validate => true do
|
20
|
+
include ::ActiveModel::Validations
|
21
|
+
|
20
22
|
integer :cost, :default => 0
|
21
23
|
validates(:cost, :numericality => {:greater_than => 0})
|
22
24
|
integer :tax
|
@@ -27,7 +29,9 @@ module Ecommerce
|
|
27
29
|
define :fedex, 2
|
28
30
|
end
|
29
31
|
|
30
|
-
struct :dimensions
|
32
|
+
struct :dimensions do
|
33
|
+
include ::ActiveModel::Validations
|
34
|
+
|
31
35
|
integer :length
|
32
36
|
integer :width
|
33
37
|
integer :height
|
@@ -74,26 +78,26 @@ module Ecommerce
|
|
74
78
|
include ::Trax::Model::Attributes::Mixin
|
75
79
|
|
76
80
|
define_attributes do
|
77
|
-
string :name, :default => "Some Shoe Name"
|
78
|
-
boolean :active, :default => true
|
81
|
+
string :name, :default => "Some Shoe Name"
|
82
|
+
boolean :active, :default => true
|
79
83
|
|
80
84
|
struct :custom_fields do
|
81
|
-
string :primary_utility, :default => "Skateboarding"
|
85
|
+
string :primary_utility, :default => "Skateboarding"
|
82
86
|
string :sole_material
|
83
|
-
boolean :has_shoelaces
|
87
|
+
boolean :has_shoelaces
|
84
88
|
integer :in_stock_quantity, :default => 0
|
85
89
|
integer :number_of_sales, :default => 0
|
86
90
|
integer :cost
|
87
91
|
integer :price
|
88
92
|
|
89
|
-
enum :color, :default => :blue
|
93
|
+
enum :color, :default => :blue do
|
90
94
|
define :red, 1
|
91
95
|
define :blue, 2
|
92
96
|
define :green, 3
|
93
97
|
define :black, 4
|
94
98
|
end
|
95
99
|
|
96
|
-
enum :size, :default => :mens_9
|
100
|
+
enum :size, :default => :mens_9 do
|
97
101
|
define :mens_6, 1
|
98
102
|
define :mens_7, 2
|
99
103
|
define :mens_8, 3
|
@@ -106,6 +110,8 @@ module Ecommerce
|
|
106
110
|
def total_profit
|
107
111
|
number_of_sales * (price - cost)
|
108
112
|
end
|
113
|
+
|
114
|
+
define_model_scopes_for(:primary_utility, :has_shoelaces, :size)
|
109
115
|
end
|
110
116
|
end
|
111
117
|
end
|
@@ -1,10 +1,10 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe ::Trax::Model::Attributes::Types::
|
3
|
+
describe ::Trax::Model::Attributes::Types::Struct, :postgres => true do
|
4
4
|
subject{ ::Ecommerce::Products::MensShoes::Fields::CustomFields }
|
5
5
|
|
6
6
|
it { expect(subject.new.primary_utility).to eq "Skateboarding" }
|
7
|
-
it { expect(subject.new.sole_material).to eq
|
7
|
+
it { expect(subject.new.sole_material).to eq nil }
|
8
8
|
|
9
9
|
context "attribute definition" do
|
10
10
|
subject { ::Ecommerce::ShippingAttributes.new }
|
@@ -4,7 +4,7 @@ describe ::Trax::Model::Attributes do
|
|
4
4
|
subject{ described_class }
|
5
5
|
|
6
6
|
describe ".register_attribute_type" do
|
7
|
-
[:array, :boolean, :enum, :integer, :
|
7
|
+
[:array, :boolean, :enum, :integer, :struct, :string, :uuid_array].each do |type|
|
8
8
|
it "registers attribute type #{type}" do
|
9
9
|
expect(subject.config.attribute_types).to have_key(type)
|
10
10
|
end
|
data/trax_model.gemspec
CHANGED
@@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.add_dependency "default_value_for", "~> 3.0.0"
|
23
23
|
spec.add_dependency "simple_enum"
|
24
24
|
spec.add_development_dependency "hashie", ">= 3.4.2"
|
25
|
+
spec.add_development_dependency "rails", "~> 4.2.0"
|
25
26
|
spec.add_development_dependency "activerecord", "~> 4.2.0"
|
26
27
|
spec.add_development_dependency "bundler", "~> 1.6"
|
27
28
|
spec.add_development_dependency "rake"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: trax_model
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.97
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jason Ayre
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-10-
|
11
|
+
date: 2015-10-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: trax_core
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: 3.4.2
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rails
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 4.2.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 4.2.0
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: activerecord
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -300,6 +314,7 @@ extra_rdoc_files: []
|
|
300
314
|
files:
|
301
315
|
- ".gitignore"
|
302
316
|
- ".rspec"
|
317
|
+
- ".travis.yml"
|
303
318
|
- Gemfile
|
304
319
|
- Guardfile
|
305
320
|
- LICENSE.txt
|
@@ -319,8 +334,8 @@ files:
|
|
319
334
|
- lib/trax/model/attributes/types/boolean.rb
|
320
335
|
- lib/trax/model/attributes/types/enum.rb
|
321
336
|
- lib/trax/model/attributes/types/integer.rb
|
322
|
-
- lib/trax/model/attributes/types/json.rb
|
323
337
|
- lib/trax/model/attributes/types/string.rb
|
338
|
+
- lib/trax/model/attributes/types/struct.rb
|
324
339
|
- lib/trax/model/attributes/types/uuid_array.rb
|
325
340
|
- lib/trax/model/attributes/value.rb
|
326
341
|
- lib/trax/model/enum.rb
|
@@ -342,7 +357,7 @@ files:
|
|
342
357
|
- lib/trax/model/scopes.rb
|
343
358
|
- lib/trax/model/sti.rb
|
344
359
|
- lib/trax/model/sti/attributes.rb
|
345
|
-
- lib/trax/model/
|
360
|
+
- lib/trax/model/struct_extensions.rb
|
346
361
|
- lib/trax/model/unique_id.rb
|
347
362
|
- lib/trax/model/uuid.rb
|
348
363
|
- lib/trax/model/uuid_array.rb
|
@@ -368,7 +383,7 @@ files:
|
|
368
383
|
- spec/trax/model/attributes/fields_spec.rb
|
369
384
|
- spec/trax/model/attributes/types/enum_spec.rb
|
370
385
|
- spec/trax/model/attributes/types/integer_spec.rb
|
371
|
-
- spec/trax/model/attributes/types/
|
386
|
+
- spec/trax/model/attributes/types/struct_spec.rb
|
372
387
|
- spec/trax/model/attributes_spec.rb
|
373
388
|
- spec/trax/model/errors_spec.rb
|
374
389
|
- spec/trax/model/freezable_spec.rb
|
@@ -377,7 +392,7 @@ files:
|
|
377
392
|
- spec/trax/model/registry_spec.rb
|
378
393
|
- spec/trax/model/restorable_spec.rb
|
379
394
|
- spec/trax/model/sti/attributes_spec.rb
|
380
|
-
- spec/trax/model/
|
395
|
+
- spec/trax/model/struct_extensions_spec.rb
|
381
396
|
- spec/trax/model/unique_id_spec.rb
|
382
397
|
- spec/trax/model/uuid_prefix_spec.rb
|
383
398
|
- spec/trax/model/uuid_spec.rb
|
@@ -422,7 +437,7 @@ test_files:
|
|
422
437
|
- spec/trax/model/attributes/fields_spec.rb
|
423
438
|
- spec/trax/model/attributes/types/enum_spec.rb
|
424
439
|
- spec/trax/model/attributes/types/integer_spec.rb
|
425
|
-
- spec/trax/model/attributes/types/
|
440
|
+
- spec/trax/model/attributes/types/struct_spec.rb
|
426
441
|
- spec/trax/model/attributes_spec.rb
|
427
442
|
- spec/trax/model/errors_spec.rb
|
428
443
|
- spec/trax/model/freezable_spec.rb
|
@@ -431,7 +446,7 @@ test_files:
|
|
431
446
|
- spec/trax/model/registry_spec.rb
|
432
447
|
- spec/trax/model/restorable_spec.rb
|
433
448
|
- spec/trax/model/sti/attributes_spec.rb
|
434
|
-
- spec/trax/model/
|
449
|
+
- spec/trax/model/struct_extensions_spec.rb
|
435
450
|
- spec/trax/model/unique_id_spec.rb
|
436
451
|
- spec/trax/model/uuid_prefix_spec.rb
|
437
452
|
- spec/trax/model/uuid_spec.rb
|
@@ -1,229 +0,0 @@
|
|
1
|
-
require 'hashie/extensions/ignore_undeclared'
|
2
|
-
|
3
|
-
module Trax
|
4
|
-
module Model
|
5
|
-
module Attributes
|
6
|
-
module Types
|
7
|
-
class Json < ::Trax::Model::Attributes::Type
|
8
|
-
module ValueExtensions
|
9
|
-
extend ::ActiveSupport::Concern
|
10
|
-
|
11
|
-
include ::ActiveModel::Validations
|
12
|
-
|
13
|
-
def inspect
|
14
|
-
self.to_hash.inspect
|
15
|
-
end
|
16
|
-
|
17
|
-
def to_json
|
18
|
-
self.to_hash.to_json
|
19
|
-
end
|
20
|
-
|
21
|
-
def to_serializable_hash
|
22
|
-
_serializable_hash = to_hash
|
23
|
-
|
24
|
-
self.class.fields_module.enums.keys.each do |attribute_name|
|
25
|
-
_serializable_hash[attribute_name] = _serializable_hash[attribute_name].try(:to_i)
|
26
|
-
end if self.class.fields_module.enums.keys.any?
|
27
|
-
|
28
|
-
_serializable_hash
|
29
|
-
end
|
30
|
-
|
31
|
-
module ClassMethods
|
32
|
-
def type; :struct end;
|
33
|
-
|
34
|
-
def permitted_keys
|
35
|
-
@permitted_keys ||= properties.map(&:to_sym)
|
36
|
-
end
|
37
|
-
|
38
|
-
#this only supports properties 1 level deep, but works beautifully
|
39
|
-
#I.E. for this structure
|
40
|
-
# define_attributes do
|
41
|
-
# struct :custom_fields do
|
42
|
-
# enum :color, :default => :blue do
|
43
|
-
# define :blue, 1
|
44
|
-
# define :red, 2
|
45
|
-
# define :green, 3
|
46
|
-
# end
|
47
|
-
# end
|
48
|
-
# end
|
49
|
-
# ::Product.by_custom_fields_color(:blue, :red)
|
50
|
-
# will return #{Product color=blue}, #{Product color=red}
|
51
|
-
def define_scopes_for_enum(attribute_name, enum_klass)
|
52
|
-
return unless has_active_record_ancestry?(enum_klass)
|
53
|
-
|
54
|
-
model_class = model_class_for_property(enum_klass)
|
55
|
-
field_name = enum_klass.parent_definition.name.demodulize.underscore
|
56
|
-
attribute_name = enum_klass.name.demodulize.underscore
|
57
|
-
scope_name = :"by_#{field_name}_#{attribute_name}"
|
58
|
-
model_class.scope(scope_name, lambda{ |*_scope_values|
|
59
|
-
_integer_values = enum_klass.select_values(*_scope_values.flat_compact_uniq!)
|
60
|
-
_integer_values.map!(&:to_s)
|
61
|
-
model_class.where("#{field_name} -> '#{attribute_name}' IN(?)", _integer_values)
|
62
|
-
})
|
63
|
-
end
|
64
|
-
|
65
|
-
def define_where_scopes_for_boolean_property(attribute_name, property_klass)
|
66
|
-
return unless has_active_record_ancestry?(property_klass)
|
67
|
-
|
68
|
-
model_class = model_class_for_property(property_klass)
|
69
|
-
field_name = property_klass.parent_definition.name.demodulize.underscore
|
70
|
-
attribute_name = property_klass.name.demodulize.underscore
|
71
|
-
scope_name = :"by_#{field_name}_#{attribute_name}"
|
72
|
-
model_class.scope(scope_name, lambda{ |*_scope_values|
|
73
|
-
_scope_values.map!(&:to_s).flat_compact_uniq!
|
74
|
-
model_class.where("#{field_name} -> '#{attribute_name}' IN(?)", _scope_values)
|
75
|
-
})
|
76
|
-
end
|
77
|
-
|
78
|
-
def define_where_scopes_for_property(attribute_name, property_klass)
|
79
|
-
return unless has_active_record_ancestry?(property_klass)
|
80
|
-
|
81
|
-
model_class = model_class_for_property(property_klass)
|
82
|
-
field_name = property_klass.parent_definition.name.demodulize.underscore
|
83
|
-
attribute_name = property_klass.name.demodulize.underscore
|
84
|
-
scope_name = :"by_#{field_name}_#{attribute_name}"
|
85
|
-
|
86
|
-
model_class.scope(scope_name, lambda{ |*_scope_values|
|
87
|
-
_scope_values.map!(&:to_s).flat_compact_uniq!
|
88
|
-
model_class.where("#{field_name} ->> '#{attribute_name}' IN(?)", _scope_values)
|
89
|
-
})
|
90
|
-
end
|
91
|
-
|
92
|
-
def has_active_record_ancestry?(property_klass)
|
93
|
-
return false unless property_klass.respond_to?(:parent_definition)
|
94
|
-
|
95
|
-
result = if property_klass.parent_definition.ancestors.include?(::ActiveRecord::Base)
|
96
|
-
true
|
97
|
-
else
|
98
|
-
has_active_record_ancestry?(property_klass.parent_definition)
|
99
|
-
end
|
100
|
-
|
101
|
-
result
|
102
|
-
end
|
103
|
-
|
104
|
-
def model_class_for_property(property_klass)
|
105
|
-
result = if property_klass.parent_definition.ancestors.include?(::ActiveRecord::Base)
|
106
|
-
property_klass.parent_definition
|
107
|
-
else
|
108
|
-
model_class_for_property(property_klass.parent_definition)
|
109
|
-
end
|
110
|
-
|
111
|
-
result
|
112
|
-
end
|
113
|
-
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
def self.define_attribute(klass, attribute_name, **options, &block)
|
118
|
-
klass_name = "#{klass.fields_module.name.underscore}/#{attribute_name}".camelize
|
119
|
-
attribute_klass = if options.key?(:extend)
|
120
|
-
_klass_prototype = options[:extend].constantize
|
121
|
-
_klass = ::Trax::Core::NamedClass.new(klass_name, _klass_prototype, :parent_definition => klass, &block)
|
122
|
-
_klass.include(ValueExtensions) unless klass.const_defined?("ValueExtensions")
|
123
|
-
_klass
|
124
|
-
else
|
125
|
-
::Trax::Core::NamedClass.new(klass_name, Value, :parent_definition => klass, &block)
|
126
|
-
end
|
127
|
-
|
128
|
-
klass.attribute(attribute_name, typecaster_klass.new(target_klass: attribute_klass))
|
129
|
-
klass.validates(attribute_name, :json_attribute => true) unless options.key?(:validate) && !options[:validate]
|
130
|
-
klass.default_value_for(attribute_name) { {} }
|
131
|
-
define_model_accessors(klass, attribute_name, attribute_klass, options[:model_accessors]) if options.key?(:model_accessors) && options[:model_accessors]
|
132
|
-
define_model_scopes(klass, attribute_name, attribute_klass, options[:model_scopes]) if options.key?(:model_scopes) && options[:model_scopes]
|
133
|
-
end
|
134
|
-
|
135
|
-
class Value < ::Trax::Model::Struct
|
136
|
-
include ValueExtensions
|
137
|
-
end
|
138
|
-
|
139
|
-
class TypeCaster < ActiveRecord::Type::Value
|
140
|
-
include ::ActiveRecord::Type::Mutable
|
141
|
-
|
142
|
-
def initialize(*args, target_klass:)
|
143
|
-
super(*args)
|
144
|
-
|
145
|
-
@target_klass = target_klass
|
146
|
-
end
|
147
|
-
|
148
|
-
def type
|
149
|
-
:json
|
150
|
-
end
|
151
|
-
|
152
|
-
def type_cast_from_user(value)
|
153
|
-
value.is_a?(@target_klass) ? value : @target_klass.new(value || {})
|
154
|
-
end
|
155
|
-
|
156
|
-
def type_cast_from_database(value)
|
157
|
-
value.present? ? @target_klass.new(::JSON.parse(value)) : value
|
158
|
-
end
|
159
|
-
|
160
|
-
def type_cast_for_database(value)
|
161
|
-
value.present? ? value.to_serializable_hash.to_json : nil
|
162
|
-
end
|
163
|
-
end
|
164
|
-
|
165
|
-
self.value_klass = ::Trax::Model::Attributes::Types::Json::Value
|
166
|
-
self.typecaster_klass = ::Trax::Model::Attributes::Types::Json::TypeCaster
|
167
|
-
|
168
|
-
private
|
169
|
-
|
170
|
-
def self.define_model_scopes(model, attribute_name, struct_attribute, option_value)
|
171
|
-
properties_to_define = if [ true ].include?(option_value)
|
172
|
-
struct_attribute.properties.to_a
|
173
|
-
elsif option_value.is_a?(Hash) && option_value.has_key?(:only)
|
174
|
-
struct_attribute.properties.to_a & option_value[:only]
|
175
|
-
elsif option_value.is_a?(Hash) && option_value.has_key?(:except)
|
176
|
-
struct_attribute.properties.to_a - option_value[:except]
|
177
|
-
elsif option_value.is_a?(Array)
|
178
|
-
struct_attribute.properties.to_a & option_value
|
179
|
-
else
|
180
|
-
raise Trax::Model::Errors::InvalidOption.new(
|
181
|
-
:option => :model_scopes,
|
182
|
-
:valid_choices => ["true", "array of properties", "hash with :only or :except keys"]
|
183
|
-
)
|
184
|
-
end
|
185
|
-
|
186
|
-
properties_to_define.each do |_property|
|
187
|
-
getter_method, setter_method = _property.to_sym, :"#{_property}="
|
188
|
-
|
189
|
-
model.__send__(:define_method, setter_method) do |val|
|
190
|
-
self[attribute_name] = {} unless self[attribute_name]
|
191
|
-
self.__send__(attribute_name).__send__(setter_method, val)
|
192
|
-
end
|
193
|
-
|
194
|
-
model.delegate(getter_method, :to => attribute_name)
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
def self.define_model_accessors(model, attribute_name, struct_attribute, option_value)
|
199
|
-
properties_to_define = if [ true ].include?(option_value)
|
200
|
-
struct_attribute.properties.to_a
|
201
|
-
elsif option_value.is_a?(Hash) && option_value.has_key?(:only)
|
202
|
-
struct_attribute.properties.to_a & option_value[:only]
|
203
|
-
elsif option_value.is_a?(Hash) && option_value.has_key?(:except)
|
204
|
-
struct_attribute.properties.to_a - option_value[:except]
|
205
|
-
elsif option_value.is_a?(Array)
|
206
|
-
struct_attribute.properties.to_a & option_value
|
207
|
-
else
|
208
|
-
raise Trax::Model::Errors::InvalidOption.new(
|
209
|
-
:option => :model_accessors,
|
210
|
-
:valid_choices => ["true", "array of properties", "hash with :only or :except keys"]
|
211
|
-
)
|
212
|
-
end
|
213
|
-
|
214
|
-
properties_to_define.each do |_property|
|
215
|
-
getter_method, setter_method = _property.to_sym, :"#{_property}="
|
216
|
-
|
217
|
-
model.__send__(:define_method, setter_method) do |val|
|
218
|
-
self[attribute_name] = {} unless self[attribute_name]
|
219
|
-
self.__send__(attribute_name).__send__(setter_method, val)
|
220
|
-
end
|
221
|
-
|
222
|
-
model.delegate(getter_method, :to => attribute_name)
|
223
|
-
end
|
224
|
-
end
|
225
|
-
end
|
226
|
-
end
|
227
|
-
end
|
228
|
-
end
|
229
|
-
end
|
data/lib/trax/model/struct.rb
DELETED
@@ -1,189 +0,0 @@
|
|
1
|
-
require 'hashie/extensions/coercion'
|
2
|
-
require 'hashie/extensions/indifferent_access'
|
3
|
-
require 'hashie/extensions/dash/indifferent_access'
|
4
|
-
|
5
|
-
module Trax
|
6
|
-
module Model
|
7
|
-
class Struct < ::Hashie::Dash
|
8
|
-
include ::Hashie::Extensions::Dash::IndifferentAccess
|
9
|
-
include ::Hashie::Extensions::Coercion
|
10
|
-
include ::Hashie::Extensions::IgnoreUndeclared
|
11
|
-
include ::Hashie::Extensions::Dash::PropertyTranslation
|
12
|
-
include ::ActiveModel::Validations
|
13
|
-
|
14
|
-
attr_reader :record
|
15
|
-
|
16
|
-
# note that we must explicitly set default or blank values for all properties.
|
17
|
-
# It defeats the whole purpose of being a 'struct'
|
18
|
-
# if we fail to do so, and it makes our data far more error prone
|
19
|
-
DEFAULT_VALUES_FOR_PROPERTY_TYPES = {
|
20
|
-
:boolean_property => nil,
|
21
|
-
:string_property => "",
|
22
|
-
:struct_property => {},
|
23
|
-
:enum_property => nil,
|
24
|
-
:integer_property => nil
|
25
|
-
}.with_indifferent_access.freeze
|
26
|
-
|
27
|
-
def self.fields_module
|
28
|
-
@fields_module ||= begin
|
29
|
-
module_name = "#{self.name}::Fields"
|
30
|
-
::Trax::Core::NamedModule.new(module_name, ::Trax::Model::Attributes::Fields)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
def self.fields
|
35
|
-
fields_module
|
36
|
-
end
|
37
|
-
|
38
|
-
def self.boolean_property(name, *args, **options, &block)
|
39
|
-
name = name.is_a?(Symbol) ? name.to_s : name
|
40
|
-
klass_name = "#{fields_module.name.underscore}/#{name}".camelize
|
41
|
-
boolean_klass = ::Trax::Core::NamedClass.new(klass_name, Trax::Model::Attributes[:boolean]::Attribute, :parent_definition => self, &block)
|
42
|
-
options[:default] = options.key?(:default) ? options[:default] : DEFAULT_VALUES_FOR_PROPERTY_TYPES[__method__]
|
43
|
-
define_where_scopes_for_boolean_property(name, boolean_klass) unless options.key?(:define_scopes) && !options[:define_scopes]
|
44
|
-
property(name, *args, **options)
|
45
|
-
end
|
46
|
-
|
47
|
-
def self.string_property(name, *args, **options, &block)
|
48
|
-
name = name.is_a?(Symbol) ? name.to_s : name
|
49
|
-
klass_name = "#{fields_module.name.underscore}/#{name}".camelize
|
50
|
-
string_klass = ::Trax::Core::NamedClass.new(klass_name, Trax::Model::Attributes[:string]::Value, :parent_definition => self, &block)
|
51
|
-
options[:default] = options.key?(:default) ? options[:default] : DEFAULT_VALUES_FOR_PROPERTY_TYPES[__method__]
|
52
|
-
define_where_scopes_for_property(name, string_klass) unless options.key?(:define_scopes) && !options[:define_scopes]
|
53
|
-
property(name.to_sym, *args, **options)
|
54
|
-
coerce_key(name.to_sym, string_klass)
|
55
|
-
end
|
56
|
-
|
57
|
-
def self.struct_property(name, *args, **options, &block)
|
58
|
-
name = name.is_a?(Symbol) ? name.to_s : name
|
59
|
-
klass_name = "#{fields_module.name.underscore}/#{name}".camelize
|
60
|
-
struct_klass = ::Trax::Core::NamedClass.new(klass_name, Trax::Model::Struct, :parent_definition => self, &block)
|
61
|
-
validates(name, :json_attribute => true) unless options[:validate] && !options[:validate]
|
62
|
-
options[:default] = options.key?(:default) ? options[:default] : DEFAULT_VALUES_FOR_PROPERTY_TYPES[__method__]
|
63
|
-
property(name.to_sym, *args, **options)
|
64
|
-
coerce_key(name.to_sym, struct_klass)
|
65
|
-
end
|
66
|
-
|
67
|
-
#note: cant validate because we are coercing which will turn it into nil
|
68
|
-
def self.enum_property(name, *args, **options, &block)
|
69
|
-
name = name.is_a?(Symbol) ? name.to_s : name
|
70
|
-
klass_name = "#{fields_module.name.underscore}/#{name}".camelize
|
71
|
-
enum_klass = ::Trax::Core::NamedClass.new(klass_name, ::Enum, :parent_definition => self, &block)
|
72
|
-
options[:default] = options.key?(:default) ? options[:default] : DEFAULT_VALUES_FOR_PROPERTY_TYPES[__method__]
|
73
|
-
define_scopes_for_enum(name, enum_klass) unless options.key?(:define_scopes) && !options[:define_scopes]
|
74
|
-
property(name.to_sym, *args, **options)
|
75
|
-
coerce_key(name.to_sym, enum_klass)
|
76
|
-
end
|
77
|
-
|
78
|
-
def self.integer_property(name, *args, **options, &block)
|
79
|
-
name = name.is_a?(Symbol) ? name.to_s : name
|
80
|
-
klass_name = "#{fields_module.name.underscore}/#{name}".camelize
|
81
|
-
integer_klass = ::Trax::Core::NamedClass.new(klass_name, ::Trax::Model::Attributes::Types::Integer::Value, :parent_definition => self, &block)
|
82
|
-
options[:default] = options.key?(:default) ? options[:default] : DEFAULT_VALUES_FOR_PROPERTY_TYPES[__method__]
|
83
|
-
define_where_scopes_for_property(name, integer_klass) if options.key?(:define_scopes) && options[:define_scopes]
|
84
|
-
property(name.to_sym, *args, **options)
|
85
|
-
coerce_key(name.to_sym, Integer)
|
86
|
-
end
|
87
|
-
|
88
|
-
def self.to_schema
|
89
|
-
::Trax::Core::Definition.new(
|
90
|
-
:source => self.name,
|
91
|
-
:name => self.name.demodulize.underscore,
|
92
|
-
:type => :struct,
|
93
|
-
:fields => self.fields_module.to_schema
|
94
|
-
)
|
95
|
-
end
|
96
|
-
|
97
|
-
def self.type; :struct end;
|
98
|
-
|
99
|
-
class << self
|
100
|
-
alias :boolean :boolean_property
|
101
|
-
alias :enum :enum_property
|
102
|
-
alias :struct :struct_property
|
103
|
-
alias :string :string_property
|
104
|
-
alias :integer :integer_property
|
105
|
-
end
|
106
|
-
|
107
|
-
def value
|
108
|
-
self
|
109
|
-
end
|
110
|
-
|
111
|
-
private
|
112
|
-
#this only supports properties 1 level deep, but works beautifully
|
113
|
-
#I.E. for this structure
|
114
|
-
# define_attributes do
|
115
|
-
# struct :custom_fields do
|
116
|
-
# enum :color, :default => :blue do
|
117
|
-
# define :blue, 1
|
118
|
-
# define :red, 2
|
119
|
-
# define :green, 3
|
120
|
-
# end
|
121
|
-
# end
|
122
|
-
# end
|
123
|
-
# ::Product.by_custom_fields_color(:blue, :red)
|
124
|
-
# will return #{Product color=blue}, #{Product color=red}
|
125
|
-
def self.define_scopes_for_enum(attribute_name, enum_klass)
|
126
|
-
return unless has_active_record_ancestry?(enum_klass)
|
127
|
-
|
128
|
-
model_class = model_class_for_property(enum_klass)
|
129
|
-
field_name = enum_klass.parent_definition.name.demodulize.underscore
|
130
|
-
attribute_name = enum_klass.name.demodulize.underscore
|
131
|
-
scope_name = :"by_#{field_name}_#{attribute_name}"
|
132
|
-
model_class.scope(scope_name, lambda{ |*_scope_values|
|
133
|
-
_integer_values = enum_klass.select_values(*_scope_values.flat_compact_uniq!)
|
134
|
-
_integer_values.map!(&:to_s)
|
135
|
-
model_class.where("#{field_name} -> '#{attribute_name}' IN(?)", _integer_values)
|
136
|
-
})
|
137
|
-
end
|
138
|
-
|
139
|
-
def self.define_where_scopes_for_boolean_property(attribute_name, property_klass)
|
140
|
-
return unless has_active_record_ancestry?(property_klass)
|
141
|
-
|
142
|
-
model_class = model_class_for_property(property_klass)
|
143
|
-
field_name = property_klass.parent_definition.name.demodulize.underscore
|
144
|
-
attribute_name = property_klass.name.demodulize.underscore
|
145
|
-
scope_name = :"by_#{field_name}_#{attribute_name}"
|
146
|
-
model_class.scope(scope_name, lambda{ |*_scope_values|
|
147
|
-
_scope_values.map!(&:to_s).flat_compact_uniq!
|
148
|
-
model_class.where("#{field_name} -> '#{attribute_name}' IN(?)", _scope_values)
|
149
|
-
})
|
150
|
-
end
|
151
|
-
|
152
|
-
def self.define_where_scopes_for_property(attribute_name, property_klass)
|
153
|
-
return unless has_active_record_ancestry?(property_klass)
|
154
|
-
|
155
|
-
model_class = model_class_for_property(property_klass)
|
156
|
-
field_name = property_klass.parent_definition.name.demodulize.underscore
|
157
|
-
attribute_name = property_klass.name.demodulize.underscore
|
158
|
-
scope_name = :"by_#{field_name}_#{attribute_name}"
|
159
|
-
|
160
|
-
model_class.scope(scope_name, lambda{ |*_scope_values|
|
161
|
-
_scope_values.map!(&:to_s).flat_compact_uniq!
|
162
|
-
model_class.where("#{field_name} ->> '#{attribute_name}' IN(?)", _scope_values)
|
163
|
-
})
|
164
|
-
end
|
165
|
-
|
166
|
-
def self.has_active_record_ancestry?(property_klass)
|
167
|
-
return false unless property_klass.respond_to?(:parent_definition)
|
168
|
-
|
169
|
-
result = if property_klass.parent_definition.ancestors.include?(::ActiveRecord::Base)
|
170
|
-
true
|
171
|
-
else
|
172
|
-
has_active_record_ancestry?(property_klass.parent_definition)
|
173
|
-
end
|
174
|
-
|
175
|
-
result
|
176
|
-
end
|
177
|
-
|
178
|
-
def self.model_class_for_property(property_klass)
|
179
|
-
result = if property_klass.parent_definition.ancestors.include?(::ActiveRecord::Base)
|
180
|
-
property_klass.parent_definition
|
181
|
-
else
|
182
|
-
model_class_for_property(property_klass.parent_definition)
|
183
|
-
end
|
184
|
-
|
185
|
-
result
|
186
|
-
end
|
187
|
-
end
|
188
|
-
end
|
189
|
-
end
|