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
@@ -5,9 +5,11 @@ module Trax
5
5
  module Attributes
6
6
  extend ::ActiveSupport::Autoload
7
7
 
8
+ autoload :Attribute
8
9
  autoload :Mixin
9
10
  autoload :Definitions
10
11
  autoload :Errors
12
+ autoload :Fields
11
13
  autoload :Types
12
14
  autoload :Type
13
15
  autoload :Value
@@ -17,7 +19,7 @@ module Trax
17
19
  end
18
20
 
19
21
  def self.register_attribute_type(mod)
20
- key = mod.name.demodulize.underscore.split('_')[0].to_sym
22
+ key = mod.name.demodulize.underscore.to_sym
21
23
 
22
24
  config.attribute_types[key] = mod
23
25
  end
@@ -0,0 +1,11 @@
1
+ module Trax
2
+ module Model
3
+ module Attributes
4
+ class Attribute
5
+ #should be an abstract class attribute, but making it a normal
6
+ #class attribute for now until https://github.com/jruby/jruby/issues/3096
7
+ class_attribute :type
8
+ end
9
+ end
10
+ end
11
+ end
@@ -13,6 +13,22 @@ module Trax
13
13
  def attribute(*args, type:, **options, &block)
14
14
  @model.trax_attribute(*args, type: type, **options, &block)
15
15
  end
16
+
17
+ def boolean(*args, **options, &block)
18
+ attribute(*args, :type => :boolean, **options, &block)
19
+ end
20
+
21
+ def enum(*args, **options, &block)
22
+ attribute(*args, type: :enum, **options, &block)
23
+ end
24
+
25
+ def string(*args, **options, &block)
26
+ attribute(*args, :type => :string, **options, &block)
27
+ end
28
+
29
+ def struct(*args, **options, &block)
30
+ attribute(*args, :type => :json, **options, &block)
31
+ end
16
32
  end
17
33
  end
18
34
  end
@@ -9,6 +9,14 @@ module Trax
9
9
  "#{type} is an unknown trax model attribute type"
10
10
  }
11
11
  end
12
+
13
+ class DefineAttributeMethodNotDefined < ::Trax::Core::Errors::Base
14
+ argument :type, :required => true
15
+
16
+ message {
17
+ "#{type} must define a define_attribute method"
18
+ }
19
+ end
12
20
  end
13
21
  end
14
22
  end
@@ -0,0 +1,74 @@
1
+ module Trax
2
+ module Model
3
+ module Attributes
4
+ module Fields
5
+ def self.extended(base)
6
+ base.module_attribute(:_blank_fields_hash) {
7
+ ::Hashie::Mash.new
8
+ }
9
+ end
10
+
11
+ def all
12
+ @all ||= begin
13
+ constants.map{|const_name| const_get(const_name) }.each_with_object(self._blank_fields_hash) do |klass, result|
14
+ result[klass.name.symbolize] = klass
15
+ end
16
+ end
17
+ end
18
+
19
+ def by_type(*type_names)
20
+ all.select{|k,v| type_names.include?(v.type) }
21
+ .try(:with_indifferent_access)
22
+ end
23
+
24
+ def each(&block)
25
+ all.values(&block)
26
+ end
27
+
28
+ def each_pair(*args, &block)
29
+ all.each_pair(*args, &block)
30
+ end
31
+
32
+ def booleans
33
+ @booleans ||= by_type(:boolean)
34
+ end
35
+
36
+ def enums
37
+ @enums ||= by_type(:enum)
38
+ end
39
+
40
+ def structs
41
+ @structs ||= by_type(:struct)
42
+ end
43
+
44
+ def strings
45
+ @strings ||= by_type(:string)
46
+ end
47
+
48
+ def to_schema
49
+ schema = all.inject(::Hashie::Mash.new) do |result, (k,v)|
50
+ case v.try(:type)
51
+ when :enum
52
+ result[k] = v.to_schema
53
+ when :struct
54
+ result[k] = v.to_schema
55
+ else
56
+ result[k] = v.try(:to_schema)
57
+ end
58
+
59
+ result
60
+ end
61
+ schema
62
+ end
63
+
64
+ def values
65
+ all.values
66
+ end
67
+
68
+ def [](_name)
69
+ const_get(_name.to_s.camelize)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -2,41 +2,70 @@ module Trax
2
2
  module Model
3
3
  module Attributes
4
4
  module Mixin
5
- def self.mixin_registry_key; :attributes end;
6
-
7
- extend ::Trax::Model::Mixin
5
+ extend ::Trax::Core::Concern
8
6
 
9
7
  included do
10
- class_attribute :trax_attribute_fields
11
-
12
- self.trax_attribute_fields = ::ActiveSupport::HashWithIndifferentAccess.new
13
-
14
8
  ::Trax::Model::Attributes.config.attribute_types.each_pair do |key, mod|
15
- include mod::Mixin
9
+ include mod::Mixin if mod.const_defined?("Mixin")
16
10
  end
17
11
  end
18
12
 
13
+ after_included do
14
+ evaluate_attribute_definitions_blocks
15
+ end
16
+
19
17
  module ClassMethods
20
- # so we can keep all our definitions in same place, and largely so we
21
- # can use attribute method to define methods
22
- #probably overkill but..
23
18
  def define_attributes(&block)
24
- model_klass_proxy = ::Trax::Model::Attributes::Definitions.new(self)
25
- model_klass_proxy.instance_eval(&block)
19
+ self.instance_variable_set("@_attribute_definitions_block", block)
26
20
  end
27
21
 
28
- def trax_attribute(name, type:, **options, &block)
29
- trax_attribute_fields[type] ||= {}
22
+ #recursively search direct parent classes for attribute definitions, so we can fully support
23
+ #inheritance
24
+ def fetch_attribute_definitions_in_chain(_attribute_definitions_blocks = [], klass=nil)
25
+ _attribute_definitions_blocks.push(klass.instance_variable_get("@_attribute_definitions_block")) if klass && klass.instance_variable_defined?("@_attribute_definitions_block")
26
+
27
+ if klass && klass.superclass != ::ActiveRecord::Base
28
+ return fetch_attribute_definitions_in_chain(_attribute_definitions_blocks, klass.superclass)
29
+ else
30
+ return _attribute_definitions_blocks.compact
31
+ end
32
+ end
30
33
 
34
+ def fields_module
35
+ @fields_module ||= begin
36
+ module_name = "#{self.name}::Fields"
37
+ ::Trax::Core::NamedModule.new(module_name, ::Trax::Model::Attributes::Fields, :definition_context => self)
38
+ end
39
+ end
40
+
41
+ def fields
42
+ @fields ||= fields_module
43
+ end
44
+
45
+ def trax_attribute(name, type:, **options, &block)
31
46
  raise ::Trax::Model::Attributes::Errors::UnknownAttributeType.new(type: type) unless ::Trax::Model::Attributes.key?(type)
32
- attribute_type_definition_method = ::Trax::Model::Attributes[type]::Mixin::ClassMethods.instance_methods.first
33
47
 
34
- self.send(attribute_type_definition_method, name, **options, &block)
48
+ if ::Trax::Model::Attributes[type].const_defined?("Mixin")
49
+ attribute_type_definition_method = ::Trax::Model::Attributes[type]::Mixin::ClassMethods.instance_methods.first
50
+ self.send(attribute_type_definition_method, name, **options, &block)
51
+ self.validates(name, options[:validates]) if options.key?(:validates)
52
+ elsif ::Trax::Model::Attributes[type].respond_to?(:define_attribute)
53
+ ::Trax::Model::Attributes[type].define_attribute(self, name, **options, &block)
54
+ self.validates(name, options[:validates]) if options.key?(:validates)
55
+ else
56
+ raise ::Trax::Model::Attributes::Errors::UnknownAttributeType.new(type: type)
57
+ end
58
+ end
59
+
60
+ def evaluate_attribute_definitions_blocks
61
+ model_klass_proxy = ::Trax::Model::Attributes::Definitions.new(self)
62
+ attribute_definition_blocks = self.superclasses_until(::ActiveRecord::Base, self, [ self ]).map{ |klass| klass.instance_variable_get(:@_attribute_definitions_block) }.compact
35
63
 
36
- self.validates(name, options[:validates]) if options.key?(:validates)
64
+ attribute_definition_blocks.each do |blk|
65
+ model_klass_proxy.instance_eval(&blk)
66
+ end if attribute_definition_blocks.any?
37
67
  end
38
68
  end
39
-
40
69
  end
41
70
  end
42
71
  end
@@ -2,6 +2,10 @@ module Trax
2
2
  module Model
3
3
  module Attributes
4
4
  class Type
5
+ class_attribute :value_klass
6
+ class_attribute :attribute_klass
7
+ class_attribute :typecaster_klass
8
+
5
9
  def self.inherited(subklass)
6
10
  ::Trax::Model::Attributes.register_attribute_type(subklass)
7
11
  end
@@ -5,6 +5,14 @@ module Trax
5
5
  module Attributes
6
6
  module Types
7
7
  class Array < ::Trax::Model::Attributes::Type
8
+ def self.define_attribute(source_klass, attribute_name, **options, &block)
9
+ klass.fields_module.const_set(attributes_klass_name, ::Class.new(::Trax::Model::Attributes[:array]::Value))
10
+ end
11
+
12
+ class Attribute < ::Trax::Model::Attributes::Attribute
13
+ self.type = :array
14
+ end
15
+
8
16
  class Value < ::Trax::Model::Attributes::Value
9
17
  class_attribute :element_class
10
18
  include ::Enumerable
@@ -60,31 +68,6 @@ module Trax
60
68
  end
61
69
  end
62
70
  end
63
-
64
- module Mixin
65
- def self.mixin_registry_key; :array_attributes end;
66
-
67
- extend ::Trax::Model::Mixin
68
- include ::Trax::Model::Attributes::Mixin
69
-
70
- module ClassMethods
71
- def array_attribute(attribute_name, **options, &block)
72
- attributes_klass_name = "#{attribute_name}_attributes".classify
73
- attributes_klass = const_set(attributes_klass_name, ::Class.new(::Trax::Model::Attributes[:array]::Value))
74
- attributes_klass.instance_eval(&block)
75
-
76
- attributes_klass.element_class = options[:of] if options.has_key?(:of)
77
-
78
- trax_attribute_fields[:array] ||= {}
79
- trax_attribute_fields[:array][attribute_name] = attributes_klass
80
-
81
- attribute(attribute_name, ::Trax::Model::Attributes[:array]::TypeCaster.new(target_klass: attributes_klass))
82
-
83
- # self.default_value_for(attribute_name) { self.class.element_class.new }
84
- # self.validates(attribute_name, :json_attribute => true)
85
- end
86
- end
87
- end
88
71
  end
89
72
  end
90
73
  end
@@ -0,0 +1,51 @@
1
+ require 'hashie/extensions/ignore_undeclared'
2
+
3
+ module Trax
4
+ module Model
5
+ module Attributes
6
+ module Types
7
+ class Boolean < ::Trax::Model::Attributes::Type
8
+ #this will by default validate boolean values
9
+ def self.define_attribute(klass, attribute_name, **options, &block)
10
+ klass_name = "#{klass.fields_module.name.underscore}/#{attribute_name}".camelize
11
+ attribute_klass = if options.key?(:class_name)
12
+ options[:class_name].constantize
13
+ else
14
+ ::Trax::Core::NamedClass.new(klass_name, ::Trax::Model::Attributes[:boolean]::Attribute, :parent_definition => klass, &block)
15
+ end
16
+
17
+ klass.attribute(attribute_name, ::Trax::Model::Attributes::Types::Boolean::TypeCaster.new)
18
+ klass.validates(attribute_name, :boolean => true) unless options.key?(:validate) && !options[:validate]
19
+ klass.default_value_for(attribute_name) { options[:default] } if options.key?(:default)
20
+ end
21
+
22
+ class Attribute < ::Trax::Model::Attributes::Attribute
23
+ self.type = :boolean
24
+
25
+ def self.to_schema
26
+ ::Trax::Core::Definition.new({
27
+ :name => attribute_name,
28
+ :type => type.to_s,
29
+ :source => name,
30
+ :values => values
31
+ })
32
+ end
33
+
34
+ private
35
+
36
+ def self.attribute_name
37
+ name.demodulize.underscore
38
+ end
39
+
40
+ def self.values
41
+ [ true, false ]
42
+ end
43
+ end
44
+
45
+ class TypeCaster < ActiveRecord::Type::Boolean
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -5,24 +5,65 @@ module Trax
5
5
  module Attributes
6
6
  module Types
7
7
  class Enum < ::Trax::Model::Attributes::Type
8
- class TypeCaster < ActiveRecord::Type::Integer
8
+ #note: we dont validate enum attribute value because typecaster will turn it into nil which we allow
9
+ def self.define_attribute(klass, attribute_name, **options, &block)
10
+ klass_name = "#{klass.fields_module.name.underscore}/#{attribute_name}".camelize
11
+ attribute_klass = if options.key?(:class_name)
12
+ options[:class_name].constantize
13
+ else
14
+ ::Trax::Core::NamedClass.new(klass_name, ::Enum, :parent_definition => klass, &block)
15
+ end
16
+
17
+ klass.attribute(attribute_name, ::Trax::Model::Attributes::Types::Enum::TypeCaster.new(target_klass: attribute_klass))
18
+ klass.default_value_for(attribute_name) { options[:default] } if options.key?(:default)
19
+ define_scopes(klass, attribute_name, attribute_klass) unless options.key?(:define_scopes) && !options[:define_scopes]
20
+ end
21
+
22
+ def self.define_scopes(klass, attribute_name, attribute_klass)
23
+ klass.class_eval do
24
+ scope_method_name = :"by_#{attribute_name}"
25
+ scope_not_method_name = :"by_#{attribute_name}_not"
26
+
27
+ scope scope_method_name, lambda { |*values|
28
+ values.flat_compact_uniq!
29
+ where(attribute_name => attribute_klass.select_values(*values))
30
+ }
31
+ scope scope_not_method_name, lambda { |*values|
32
+ values.flat_compact_uniq!
33
+ where.not(attribute_name => attribute_klass.select_values(*values))
34
+ }
35
+ end
9
36
  end
10
37
 
11
- module Mixin
12
- def self.mixin_registry_key; :enum_attributes end;
38
+ class TypeCaster < ActiveRecord::Type::Value
39
+ include ::ActiveRecord::Type::Mutable
13
40
 
14
- extend ::Trax::Model::Mixin
15
- include ::Trax::Model::Attributes::Mixin
16
- include ::Trax::Model::Enum
41
+ def type; :enum end;
17
42
 
18
- module ClassMethods
19
- def enum_attribute(attribute_name, values:, **options, &block)
20
- attribute(attribute_name, ::Trax::Model::Attributes[:enum]::TypeCaster.new)
43
+ def initialize(*args, target_klass:)
44
+ super(*args)
45
+
46
+ @target_klass = target_klass
47
+ end
21
48
 
22
- options.delete(:validates) if options.key?(:validates)
49
+ def type_cast_from_user(value)
50
+ @target_klass === value ? @target_klass.new(value) : nil
51
+ end
52
+
53
+ def type_cast_from_database(value)
54
+ return if value.nil?
55
+
56
+ value.present? ? @target_klass.new(value.to_i) : value
57
+ end
58
+
59
+ def type_cast_for_database(value)
60
+ return if value.nil?
61
+
62
+ value.try(:to_i) { @target_klass.new(value).to_i }
63
+ end
23
64
 
24
- as_enum(attribute_name, values, **options)
25
- end
65
+ def changed_in_place?(raw_old_value, new_value)
66
+ raw_old_value.try(:to_i) != type_cast_for_database(new_value)
26
67
  end
27
68
  end
28
69
  end
@@ -5,14 +5,43 @@ module Trax
5
5
  module Attributes
6
6
  module Types
7
7
  class Json < ::Trax::Model::Attributes::Type
8
- class Value < ::Trax::Model::Struct
9
- def self.permitted_keys
10
- @permitted_keys ||= properties.map(&:to_sym)
11
- end
8
+ module ValueExtensions
9
+ extend ::ActiveSupport::Concern
12
10
 
13
11
  def inspect
14
12
  self.to_hash.inspect
15
13
  end
14
+
15
+ def to_json
16
+ self.to_hash.to_json
17
+ end
18
+
19
+ module ClassMethods
20
+ def type; :struct end;
21
+
22
+ def permitted_keys
23
+ @permitted_keys ||= properties.map(&:to_sym)
24
+ end
25
+ end
26
+ end
27
+
28
+ def self.define_attribute(klass, attribute_name, **options, &block)
29
+ klass_name = "#{klass.fields_module.name.underscore}/#{attribute_name}".camelize
30
+ attribute_klass = if options.key?(:class_name)
31
+ _klass = options[:class_name].constantize
32
+ _klass.include(ValueExtensions)
33
+ _klass
34
+ else
35
+ ::Trax::Core::NamedClass.new(klass_name, Value, :parent_definition => klass, &block)
36
+ end
37
+
38
+ klass.attribute(attribute_name, typecaster_klass.new(target_klass: attribute_klass))
39
+ klass.validates(attribute_name, :json_attribute => true) unless options.key?(:validate) && !options[:validate]
40
+ klass.default_value_for(attribute_name) { {} }
41
+ end
42
+
43
+ class Value < ::Trax::Model::Struct
44
+ include ValueExtensions
16
45
  end
17
46
 
18
47
  class TypeCaster < ActiveRecord::Type::Value
@@ -37,38 +66,12 @@ module Trax
37
66
  end
38
67
 
39
68
  def type_cast_for_database(value)
40
- value.present? ? value.to_hash.to_json : nil
69
+ value.present? ? value.to_serializable_hash.to_json : nil
41
70
  end
42
71
  end
43
72
 
44
- module Mixin
45
- def self.mixin_registry_key; :json_attributes end;
46
-
47
- extend ::Trax::Model::Mixin
48
- include ::Trax::Model::Attributes::Mixin
49
-
50
- included do
51
- class_attribute :json_attribute_fields
52
-
53
- self.json_attribute_fields = ::ActiveSupport::HashWithIndifferentAccess.new
54
- end
55
-
56
- module ClassMethods
57
- def json_attribute(attribute_name, **options, &block)
58
- attributes_klass_name = "#{attribute_name}_attributes".classify
59
- attributes_klass = const_set(attributes_klass_name, ::Class.new(::Trax::Model::Attributes[:json]::Value))
60
- attributes_klass.instance_eval(&block)
61
-
62
- trax_attribute_fields[:json] ||= {}
63
- trax_attribute_fields[:json][attribute_name] = attributes_klass
64
-
65
- attribute(attribute_name, ::Trax::Model::Attributes[:json]::TypeCaster.new(target_klass: attributes_klass))
66
-
67
- self.default_value_for(attribute_name) { {} }
68
- self.validates(attribute_name, :json_attribute => true) unless options.key?(:validate) && !options[:validate]
69
- end
70
- end
71
- end
73
+ self.value_klass = ::Trax::Model::Attributes::Types::Json::Value
74
+ self.typecaster_klass = ::Trax::Model::Attributes::Types::Json::TypeCaster
72
75
  end
73
76
  end
74
77
  end