trax_model 0.0.96 → 0.0.97
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/.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
|