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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 41cea382b5de1b943f513df7167f86adb4912f94
4
- data.tar.gz: 86a810a545fe00b3de9b759f1be14b82fa632069
3
+ metadata.gz: fe39dac9ff74e51159a879a215dc98ccc0bd78c4
4
+ data.tar.gz: 373428f4903798d45be49fa6d9b539a68c5ed890
5
5
  SHA512:
6
- metadata.gz: 93c99a50811671b71bb4a4156ac3200e019d28a03611a80e8ddb0cc9184d830d8129d9f20f6c88c67ec2b93e03bfd82684bbdfb7df7da99c683ba54c1fd2b7bd
7
- data.tar.gz: 2b90612a719954a3f0027c6792d14af4a5e445a99b15fd392b57ea597d5439354e54cec3530f223a44cdf46f5f0e5ca0a840e6ddf012cfac4a06ce937334b254
6
+ metadata.gz: ddd740eab7478af4d5b6b3c02556d281191899d8f5ad6ae1882d937e38120f0c759a2970d32dd58fed65d806928e3fc53a02ccf89efff3f4d1afc8a93fb6dba8
7
+ data.tar.gz: e71ce067fa72afa23bf4149849fd29d37a4e6eb823e2033d0a96835fbc1592ec3e537cf2f99fca3ba1ddd249ab3a8a9dc6675fe33fd64eb2e7e7252c4561e1c6
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ script: bundle exec rspec
3
+ rvm:
4
+ - 2.2
5
+ - 2.1
6
+ notifications:
7
+ email: false
data/Gemfile CHANGED
@@ -1,2 +1,3 @@
1
1
  source 'https://rubygems.org'
2
+
2
3
  gemspec
@@ -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)
@@ -31,7 +31,7 @@ module Trax
31
31
  end
32
32
 
33
33
  def struct(*args, **options, &block)
34
- attribute(*args, :type => :json, **options, &block)
34
+ attribute(*args, :type => :struct, **options, &block)
35
35
  end
36
36
  end
37
37
  end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module TraxModel
2
- VERSION = '0.0.96'
2
+ VERSION = '0.0.97'
3
3
  end
@@ -3,6 +3,7 @@ require 'bundler'
3
3
  require 'simplecov'
4
4
  # require 'pry'
5
5
  require 'trax_model'
6
+ require 'rails'
6
7
  require 'active_record'
7
8
 
8
9
  SimpleCov.start do
@@ -114,9 +114,11 @@ end
114
114
  class SwinglineStaplerAttributeSet < ::ActiveRecord::Base
115
115
  end
116
116
 
117
- require 'trax/model/struct'
118
117
 
119
- class StoreCategory < ::Trax::Model::Struct
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
@@ -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, :validate => true do
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", :define_scopes => true
78
- boolean :active, :default => true, :define_scopes => 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", :define_scopes => true
85
+ string :primary_utility, :default => "Skateboarding"
82
86
  string :sole_material
83
- boolean :has_shoelaces, :define_scopes => true
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, :define_scopes => false do
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, :define_scopes => true do
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::Json, :postgres => true do
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, :json, :string, :uuid_array].each do |type|
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
@@ -1,5 +1,5 @@
1
1
  require 'spec_helper'
2
- describe ::Trax::Model::Struct do
2
+ describe ::Trax::Model::StructExtensions do
3
3
  subject {
4
4
  ::StoreCategory.new(
5
5
  "name" => "watches",
@@ -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.96
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-02 00:00:00.000000000 Z
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/struct.rb
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/json_spec.rb
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/struct_spec.rb
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/json_spec.rb
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/struct_spec.rb
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
@@ -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