trax_model 0.0.92 → 0.0.93

Sign up to get free protection for your applications and to get access to all the features.
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