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 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