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