trax_model 0.0.92 → 0.0.93

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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +0 -1
  3. data/README.md +227 -86
  4. data/lib/trax.rb +0 -1
  5. data/lib/trax/model.rb +23 -29
  6. data/lib/trax/model/attributes.rb +3 -1
  7. data/lib/trax/model/attributes/attribute.rb +11 -0
  8. data/lib/trax/model/attributes/definitions.rb +16 -0
  9. data/lib/trax/model/attributes/errors.rb +8 -0
  10. data/lib/trax/model/attributes/fields.rb +74 -0
  11. data/lib/trax/model/attributes/mixin.rb +48 -19
  12. data/lib/trax/model/attributes/type.rb +4 -0
  13. data/lib/trax/model/attributes/types/array.rb +8 -25
  14. data/lib/trax/model/attributes/types/boolean.rb +51 -0
  15. data/lib/trax/model/attributes/types/enum.rb +53 -12
  16. data/lib/trax/model/attributes/types/json.rb +36 -33
  17. data/lib/trax/model/attributes/types/string.rb +50 -0
  18. data/lib/trax/model/attributes/types/uuid_array.rb +17 -28
  19. data/lib/trax/model/attributes/value.rb +16 -0
  20. data/lib/trax/model/errors.rb +7 -0
  21. data/lib/trax/model/mixins.rb +11 -0
  22. data/lib/trax/model/mixins/field_scopes.rb +60 -0
  23. data/lib/trax/model/mixins/id_scopes.rb +36 -0
  24. data/lib/trax/model/mixins/sort_by_scopes.rb +25 -0
  25. data/lib/trax/model/railtie.rb +1 -0
  26. data/lib/trax/model/scopes.rb +16 -0
  27. data/lib/trax/model/struct.rb +168 -14
  28. data/lib/trax/model/unique_id.rb +14 -21
  29. data/lib/trax/model/uuid.rb +1 -1
  30. data/lib/trax/validators/enum_attribute_validator.rb +9 -0
  31. data/lib/trax/validators/future_validator.rb +1 -1
  32. data/lib/trax/validators/json_attribute_validator.rb +3 -3
  33. data/lib/trax/validators/string_attribute_validator.rb +17 -0
  34. data/lib/trax_model/version.rb +1 -1
  35. data/spec/db/database.yml +16 -0
  36. data/spec/db/schema/default_tables.rb +68 -0
  37. data/spec/db/schema/pg_tables.rb +27 -0
  38. data/spec/spec_helper.rb +20 -3
  39. data/spec/support/models.rb +123 -0
  40. data/spec/support/pg/models.rb +103 -0
  41. data/spec/trax/model/attributes/fields_spec.rb +88 -0
  42. data/spec/trax/model/attributes/types/enum_spec.rb +51 -0
  43. data/spec/trax/model/attributes/types/json_spec.rb +107 -0
  44. data/spec/trax/model/attributes_spec.rb +13 -0
  45. data/spec/trax/model/errors_spec.rb +1 -2
  46. data/spec/trax/model/mixins/field_scopes_spec.rb +7 -0
  47. data/spec/trax/model/struct_spec.rb +1 -1
  48. data/spec/trax/model/unique_id_spec.rb +1 -3
  49. data/spec/trax/validators/url_validator_spec.rb +1 -1
  50. data/trax_model.gemspec +4 -4
  51. metadata +57 -19
  52. data/lib/trax/model/config.rb +0 -16
  53. data/lib/trax/model/validators.rb +0 -15
  54. data/lib/trax/validators/enum_validator.rb +0 -16
  55. data/spec/support/schema.rb +0 -151
  56. data/spec/trax/model/config_spec.rb +0 -13
@@ -0,0 +1,50 @@
1
+ require 'hashie/extensions/ignore_undeclared'
2
+
3
+ module Trax
4
+ module Model
5
+ module Attributes
6
+ module Types
7
+ class String < ::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?(:class_name)
11
+ options[:class_name].constantize
12
+ else
13
+ ::Trax::Core::NamedClass.new(klass_name, Value, :parent_definition => klass, &block)
14
+ end
15
+
16
+ klass.attribute(attribute_name, typecaster_klass.new(target_klass: attribute_klass))
17
+ klass.default_value_for(attribute_name) { options[:default] } if options.key?(:default)
18
+ end
19
+
20
+ class Value < ::Trax::Model::Attributes::Value
21
+ def self.type; :string end;
22
+ end
23
+
24
+ class TypeCaster < ActiveRecord::Type::String
25
+ def initialize(*args, target_klass:)
26
+ super(*args)
27
+
28
+ @target_klass = target_klass
29
+ end
30
+
31
+ def type_cast_from_user(value)
32
+ value.is_a?(@target_klass) ? @target_klass : @target_klass.new(value || {})
33
+ end
34
+
35
+ def type_cast_from_database(value)
36
+ value.present? ? @target_klass.new(value) : value
37
+ end
38
+
39
+ def type_cast_for_database(value)
40
+ value.try(:to_s)
41
+ end
42
+ end
43
+
44
+ self.value_klass = ::Trax::Model::Attributes::Types::String::Value
45
+ self.typecaster_klass = ::Trax::Model::Attributes::Types::String::TypeCaster
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -5,6 +5,23 @@ module Trax
5
5
  module Attributes
6
6
  module Types
7
7
  class UuidArray < ::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?(:class_name)
11
+ options[:class_name].constantize
12
+ else
13
+ ::Trax::Core::NamedClass.new(klass_name, Value, :parent_definition => klass, &block)
14
+ end
15
+
16
+ attribute_klass.element_class = options[:of] if options.has_key?(:of)
17
+ options.has_key?(:default) ? self.default_value_for(attribute_name, options[:default]) : []
18
+ klass.attribute(attribute_name, typecaster_klass.new(target_klass: attribute_klass))
19
+ end
20
+
21
+ class Attribute < ::Trax::Model::Attributes::Attribute
22
+ self.type = :uuid_array
23
+ end
24
+
8
25
  class Value < ::Trax::Model::Attributes::Value
9
26
  def initialize(*args)
10
27
  @array = ::Trax::Model::UUIDArray.new(*args)
@@ -48,34 +65,6 @@ module Trax
48
65
  end
49
66
  end
50
67
  end
51
-
52
- module Mixin
53
- def self.mixin_registry_key; :uuid_array_attributes end;
54
-
55
- extend ::Trax::Model::Mixin
56
- include ::Trax::Model::Attributes::Mixin
57
-
58
- module ClassMethods
59
- def uuid_array_attribute(attribute_name, **options, &block)
60
- attributes_klass_name = "#{attribute_name}_attributes".classify
61
- attributes_klass = const_set(attributes_klass_name, ::Class.new(::Trax::Model::Attributes[:uuid_array]::Value))
62
- attributes_klass.instance_eval(&block) if block_given?
63
-
64
- attributes_klass.element_class = options[:of] if options.has_key?(:of)
65
-
66
- trax_attribute_fields[:uuid_array] ||= {}
67
- trax_attribute_fields[:uuid_array][attribute_name] = attributes_klass
68
-
69
- if options.has_key?(:default)
70
- self.default_value_for(attribute_name, options[:default])
71
- else
72
- []
73
- end
74
-
75
- attribute(attribute_name, ::Trax::Model::Attributes[:uuid_array]::TypeCaster.new(target_klass: attributes_klass))
76
- end
77
- end
78
- end
79
68
  end
80
69
  end
81
70
  end
@@ -2,7 +2,23 @@ module Trax
2
2
  module Model
3
3
  module Attributes
4
4
  class Value < SimpleDelegator
5
+ include ::ActiveModel::Validations
5
6
 
7
+ def initialize(val)
8
+ @value = val
9
+ end
10
+
11
+ def __getobj__
12
+ @value
13
+ end
14
+
15
+ def self.symbolic_name
16
+ name.demodulize.underscore.to_sym
17
+ end
18
+
19
+ def self.to_sym
20
+ :value
21
+ end
6
22
  end
7
23
  end
8
24
  end
@@ -34,6 +34,13 @@ module Trax
34
34
  }
35
35
  end
36
36
 
37
+ class FieldDoesNotExist < Trax::Core::Errors::Base
38
+ argument :field, :required => true
39
+ argument :model, :required => true
40
+
41
+ message { "Field #{field} does not exist for #{model}" }
42
+ end
43
+
37
44
  class STIAttributeNotFound < ::Trax::Core::Errors::Base
38
45
  argument :attribute_name
39
46
 
@@ -0,0 +1,11 @@
1
+ module Trax
2
+ module Model
3
+ module Mixins
4
+ extend ::ActiveSupport::Autoload
5
+
6
+ autoload :FieldScopes
7
+ autoload :IdScopes
8
+ autoload :SortByScopes
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,60 @@
1
+ module Trax
2
+ module Model
3
+ module Mixins
4
+ module FieldScopes
5
+ extend ::Trax::Model::Mixin
6
+
7
+ mixed_in do |**options|
8
+ options.each_pair do |field_scope_name, field_scope_options|
9
+ field_scope_options = {} if [ true, false ].include?(field_scope_options)
10
+
11
+ field_scope_options[:field] ||= field_scope_name.to_s.include?("by_") ? field_scope_name.to_s.split("by_").pop.to_sym : field_scope_name
12
+ field_scope_options[:type] ||= :where
13
+
14
+ case field_scope_options[:type]
15
+ when :where
16
+ define_where_scope_for_field(field_scope_name, **field_scope_options)
17
+ when :match
18
+ define_matching_scope_for_field(field_scope_name, **field_scope_options)
19
+ when :matching
20
+ define_matching_scope_for_field(field_scope_name, **field_scope_options)
21
+ when :not
22
+ define_where_not_scope_for_field(field_scope_name, **field_scope_options)
23
+ when :where_not
24
+ define_where_not_scope_for_field(field_scope_name, **field_scope_options)
25
+ else
26
+ define_where_scope_for_field(field_scope_name, **field_scope_options)
27
+ end
28
+ end
29
+ end
30
+
31
+ module ClassMethods
32
+ private
33
+ def define_where_scope_for_field(field_scope_name, **options)
34
+ scope field_scope_name, lambda{ |*_values|
35
+ _values.flat_compact_uniq!
36
+ where(options[:field] => _values)
37
+ }
38
+
39
+ # Alias scope names with pluralized versions, i.e. by_id also => by_ids
40
+ singleton_class.__send__(:alias_method, :"#{field_scope_name.to_s.pluralize}", field_scope_name)
41
+ end
42
+
43
+ def define_where_not_scope_for_field(field_scope_name, **options)
44
+ scope field_scope_name, lambda{ |*_values|
45
+ _values.flat_compact_uniq!
46
+ where.not(options[:field] => _values)
47
+ }
48
+ end
49
+
50
+ def define_matching_scope_for_field(field_scope_name, **options)
51
+ scope field_scope_name, lambda{ |*_values|
52
+ _values.flat_compact_uniq!
53
+ matching(options[:field] => _values)
54
+ }
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,36 @@
1
+ #note this only works with postgres UUID column Types
2
+
3
+ module Trax
4
+ module Model
5
+ module Mixins
6
+ module IdScopes
7
+ extend ::Trax::Model::Mixin
8
+
9
+ included do
10
+ if table_exists?
11
+ id_column_names = self.columns.select{ |col| col.sql_type == "uuid" }.map(&:name).map(&:to_sym)
12
+ id_column_names.each do |_id_column_name|
13
+ scope :"by_#{_id_column_name}", lambda{ |*_record_ids|
14
+ _record_ids.flat_compact_uniq!
15
+ where(_id_column_name => _record_ids)
16
+ }
17
+
18
+ scope :"by_#{_id_column_name}_not", lambda{ |*_record_ids|
19
+ _record_ids.flat_compact_uniq!
20
+ where.not(_id_column_name => _record_ids)
21
+ }
22
+
23
+ define_singleton_method(:"by_#{_id_column_name}s") do |*_record_ids|
24
+ __send__("by_#{_id_column_name}", _record_ids)
25
+ end
26
+
27
+ define_singleton_method(:"by_#{_id_column_name}s_not") do |*_record_ids|
28
+ __send__("by_#{_id_column_name}_not", _record_ids)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,25 @@
1
+ module Trax
2
+ module Model
3
+ module Mixins
4
+ module SortByScopes
5
+ extend ::Trax::Model::Mixin
6
+
7
+ included do
8
+ scope :sort_by_most_recent, lambda{|field_name='created_at'|
9
+ order("#{field_name} DESC")
10
+ }
11
+ scope :sort_by_least_recent, lambda{|field_name='created_at'|
12
+ order("#{field_name} ASC")
13
+ }
14
+
15
+ class << self
16
+ alias_method :sort_by_newest, :sort_by_most_recent
17
+ alias_method :sort_by_oldest, :sort_by_least_recent
18
+ alias_method :by_newest, :sort_by_most_recent
19
+ alias_method :by_oldest, :sort_by_least_recent
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -4,6 +4,7 @@ module Trax
4
4
  ::ActiveSupport.on_load(:active_record) do
5
5
  def self.inherited(subklass)
6
6
  subklass.include(::Trax::Model) if ::Trax::Model.config.auto_include
7
+ subklass.include(::Trax::Model::Attributes::Mixin) if ::Trax::Model.config.auto_include
7
8
 
8
9
  super(subklass)
9
10
 
@@ -0,0 +1,16 @@
1
+ module Trax
2
+ module Model
3
+ module Scopes
4
+ extend ::ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ def field_scope(attr_name)
8
+ scope attr_name, lambda{ |*_scope_values|
9
+ _scope_values.flat_compact_uniq!
10
+ where(attr_name => _scope_values)
11
+ }
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,30 +1,184 @@
1
+ require 'hashie/extensions/coercion'
2
+ require 'hashie/extensions/indifferent_access'
3
+ require 'hashie/extensions/dash/indifferent_access'
4
+
1
5
  module Trax
2
6
  module Model
3
7
  class Struct < ::Hashie::Dash
4
8
  include ::Hashie::Extensions::Dash::IndifferentAccess
5
9
  include ::Hashie::Extensions::Coercion
6
10
  include ::Hashie::Extensions::IgnoreUndeclared
11
+ include ::Hashie::Extensions::Dash::PropertyTranslation
7
12
  include ::ActiveModel::Validations
8
13
 
9
- class_attribute :property_types
10
-
11
- def self.inherited(subklass)
12
- super(subklass)
14
+ # note that we must explicitly set default or blank values for all properties.
15
+ # It defeats the whole purpose of being a 'struct'
16
+ # if we fail to do so, and it makes our data far more error prone
17
+ DEFAULT_VALUES_FOR_PROPERTY_TYPES = {
18
+ :boolean_property => nil,
19
+ :string_property => "",
20
+ :struct_property => {},
21
+ :enum_property => nil,
22
+ }.with_indifferent_access.freeze
13
23
 
14
- subklass.property_types = {}.dup.tap do |hash|
15
- hash[:structs] = []
16
- hash
24
+ def self.fields_module
25
+ @fields_module ||= begin
26
+ module_name = "#{self.name}::Fields"
27
+ ::Trax::Core::NamedModule.new(module_name, ::Trax::Model::Attributes::Fields)
17
28
  end
18
29
  end
19
30
 
20
- def self.struct_property(name, *args, **options, &block)
21
- struct_klass_name = "#{name}_structs".classify
22
- struct_klass = const_set(struct_klass_name, ::Class.new(::Trax::Model::Struct))
23
- struct_klass.instance_eval(&block)
24
- options[:default] = {} unless options.key?(:default)
31
+ def self.fields
32
+ fields_module
33
+ end
34
+
35
+ def self.boolean_property(name, *args, **options, &block)
36
+ name = name.is_a?(Symbol) ? name.to_s : name
37
+ klass_name = "#{fields_module.name.underscore}/#{name}".camelize
38
+ boolean_klass = ::Trax::Core::NamedClass.new(klass_name, Trax::Model::Attributes[:boolean]::Attribute, :parent_definition => self, &block)
39
+ options[:default] = options.key?(:default) ? options[:default] : DEFAULT_VALUES_FOR_PROPERTY_TYPES[__method__]
40
+ define_where_scopes_for_boolean_property(name, boolean_klass) unless options.key?(:define_scopes) && !options[:define_scopes]
25
41
  property(name, *args, **options)
26
- coerce_key(name, struct_klass)
27
- property_types[:structs].push(name)
42
+ end
43
+
44
+ def self.string_property(name, *args, **options, &block)
45
+ name = name.is_a?(Symbol) ? name.to_s : name
46
+ klass_name = "#{fields_module.name.underscore}/#{name}".camelize
47
+ string_klass = ::Trax::Core::NamedClass.new(klass_name, Trax::Model::Attributes[:string]::Value, :parent_definition => self, &block)
48
+ options[:default] = options.key?(:default) ? options[:default] : DEFAULT_VALUES_FOR_PROPERTY_TYPES[__method__]
49
+ define_where_scopes_for_property(name, string_klass) unless options.key?(:define_scopes) && !options[:define_scopes]
50
+ property(name.to_sym, *args, **options)
51
+ coerce_key(name.to_sym, string_klass)
52
+ end
53
+
54
+ def self.struct_property(name, *args, **options, &block)
55
+ name = name.is_a?(Symbol) ? name.to_s : name
56
+ klass_name = "#{fields_module.name.underscore}/#{name}".camelize
57
+ struct_klass = ::Trax::Core::NamedClass.new(klass_name, Trax::Model::Struct, :parent_definition => self, &block)
58
+ validates(name, :json_attribute => true) unless options[:validate] && !options[:validate]
59
+ options[:default] = options.key?(:default) ? options[:default] : DEFAULT_VALUES_FOR_PROPERTY_TYPES[__method__]
60
+ property(name.to_sym, *args, **options)
61
+ coerce_key(name.to_sym, struct_klass)
62
+ end
63
+
64
+ #note: cant validate because we are coercing which will turn it into nil
65
+ def self.enum_property(name, *args, **options, &block)
66
+ name = name.is_a?(Symbol) ? name.to_s : name
67
+ klass_name = "#{fields_module.name.underscore}/#{name}".camelize
68
+ enum_klass = ::Trax::Core::NamedClass.new(klass_name, ::Enum, :parent_definition => self, &block)
69
+ options[:default] = options.key?(:default) ? options[:default] : DEFAULT_VALUES_FOR_PROPERTY_TYPES[__method__]
70
+ define_scopes_for_enum(name, enum_klass) unless options.key?(:define_scopes) && !options[:define_scopes]
71
+ property(name.to_sym, *args, **options)
72
+ coerce_key(name.to_sym, enum_klass)
73
+ end
74
+
75
+ def self.to_schema
76
+ ::Trax::Core::Definition.new(
77
+ :source => self.name,
78
+ :name => self.name.demodulize.underscore,
79
+ :type => :struct,
80
+ :fields => self.fields_module.to_schema
81
+ )
82
+ end
83
+
84
+ def self.type; :struct end;
85
+
86
+ def to_serializable_hash
87
+ _serializable_hash = to_hash
88
+
89
+ self.class.fields_module.enums.keys.each do |attribute_name|
90
+ _serializable_hash[attribute_name] = _serializable_hash[attribute_name].try(:to_i)
91
+ end if self.class.fields_module.enums.keys.any?
92
+
93
+ _serializable_hash
94
+ end
95
+
96
+ class << self
97
+ alias :boolean :boolean_property
98
+ alias :enum :enum_property
99
+ alias :struct :struct_property
100
+ alias :string :string_property
101
+ end
102
+
103
+ def value
104
+ self
105
+ end
106
+
107
+ private
108
+ #this only supports properties 1 level deep, but works beautifully
109
+ #I.E. for this structure
110
+ # define_attributes do
111
+ # struct :custom_fields do
112
+ # enum :color, :default => :blue do
113
+ # define :blue, 1
114
+ # define :red, 2
115
+ # define :green, 3
116
+ # end
117
+ # end
118
+ # end
119
+ # ::Product.by_custom_fields_color(:blue, :red)
120
+ # will return #{Product color=blue}, #{Product color=red}
121
+ def self.define_scopes_for_enum(attribute_name, enum_klass)
122
+ return unless has_active_record_ancestry?(enum_klass)
123
+
124
+ model_class = model_class_for_property(enum_klass)
125
+ field_name = enum_klass.parent_definition.name.demodulize.underscore
126
+ attribute_name = enum_klass.name.demodulize.underscore
127
+ scope_name = :"by_#{field_name}_#{attribute_name}"
128
+ model_class.scope(scope_name, lambda{ |*_scope_values|
129
+ _integer_values = enum_klass.select_values(*_scope_values.flat_compact_uniq!)
130
+ _integer_values.map!(&:to_s)
131
+ model_class.where("#{field_name} -> '#{attribute_name}' IN(?)", _integer_values)
132
+ })
133
+ end
134
+
135
+ def self.define_where_scopes_for_boolean_property(attribute_name, property_klass)
136
+ return unless has_active_record_ancestry?(property_klass)
137
+
138
+ model_class = model_class_for_property(property_klass)
139
+ field_name = property_klass.parent_definition.name.demodulize.underscore
140
+ attribute_name = property_klass.name.demodulize.underscore
141
+ scope_name = :"by_#{field_name}_#{attribute_name}"
142
+ model_class.scope(scope_name, lambda{ |*_scope_values|
143
+ _scope_values.map!(&:to_s).flat_compact_uniq!
144
+ model_class.where("#{field_name} -> '#{attribute_name}' IN(?)", _scope_values)
145
+ })
146
+ end
147
+
148
+ def self.define_where_scopes_for_property(attribute_name, property_klass)
149
+ return unless has_active_record_ancestry?(property_klass)
150
+
151
+ model_class = model_class_for_property(property_klass)
152
+ field_name = property_klass.parent_definition.name.demodulize.underscore
153
+ attribute_name = property_klass.name.demodulize.underscore
154
+ scope_name = :"by_#{field_name}_#{attribute_name}"
155
+
156
+ model_class.scope(scope_name, lambda{ |*_scope_values|
157
+ _scope_values.map!(&:to_s).flat_compact_uniq!
158
+ model_class.where("#{field_name} ->> '#{attribute_name}' IN(?)", _scope_values)
159
+ })
160
+ end
161
+
162
+ def self.has_active_record_ancestry?(property_klass)
163
+ return false unless property_klass.respond_to?(:parent_definition)
164
+
165
+ result = if property_klass.parent_definition.ancestors.include?(::ActiveRecord::Base)
166
+ true
167
+ else
168
+ has_active_record_ancestry?(property_klass.parent_definition)
169
+ end
170
+
171
+ result
172
+ end
173
+
174
+ def self.model_class_for_property(property_klass)
175
+ result = if property_klass.parent_definition.ancestors.include?(::ActiveRecord::Base)
176
+ property_klass.parent_definition
177
+ else
178
+ model_class_for_property(property_klass.parent_definition)
179
+ end
180
+
181
+ result
28
182
  end
29
183
  end
30
184
  end