axle_attributes 1.13.2

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 (119) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.travis.yml +16 -0
  4. data/Gemfile +15 -0
  5. data/README.md +10 -0
  6. data/Rakefile +18 -0
  7. data/app/models/attribute_customization.rb +7 -0
  8. data/app/models/attribute_customization/local_cache.rb +28 -0
  9. data/app/models/attribute_mapping.rb +15 -0
  10. data/app/models/attribute_mapping/local_cache.rb +40 -0
  11. data/axle_attributes.gemspec +22 -0
  12. data/bin/rails +8 -0
  13. data/db/migrate/20131118042651_create_attribute_customizations.rb +16 -0
  14. data/db/migrate/20131121013237_create_attribute_mappings.rb +17 -0
  15. data/db/migrate/20151120160522_add_editable_to_attribute_customizations.rb +7 -0
  16. data/lib/axle_attributes.rb +30 -0
  17. data/lib/axle_attributes/child_definition.rb +42 -0
  18. data/lib/axle_attributes/conversions.rb +36 -0
  19. data/lib/axle_attributes/definition.rb +118 -0
  20. data/lib/axle_attributes/definition/callbacks.rb +34 -0
  21. data/lib/axle_attributes/definition/customization.rb +51 -0
  22. data/lib/axle_attributes/definition/formatted.rb +57 -0
  23. data/lib/axle_attributes/definition/indexed.rb +41 -0
  24. data/lib/axle_attributes/definition/mappings.rb +48 -0
  25. data/lib/axle_attributes/dumper.rb +54 -0
  26. data/lib/axle_attributes/engine.rb +4 -0
  27. data/lib/axle_attributes/format.rb +74 -0
  28. data/lib/axle_attributes/has_attributes.rb +104 -0
  29. data/lib/axle_attributes/has_attributes/json_reader.rb +87 -0
  30. data/lib/axle_attributes/model.rb +34 -0
  31. data/lib/axle_attributes/null_definition.rb +11 -0
  32. data/lib/axle_attributes/parent_definition.rb +37 -0
  33. data/lib/axle_attributes/provided.rb +67 -0
  34. data/lib/axle_attributes/regex.rb +4 -0
  35. data/lib/axle_attributes/segmented.rb +30 -0
  36. data/lib/axle_attributes/serializations.rb +147 -0
  37. data/lib/axle_attributes/serializations/builder/serialization.rb +83 -0
  38. data/lib/axle_attributes/serializations/builder/serialize_many.rb +17 -0
  39. data/lib/axle_attributes/serializations/builder/serialize_one.rb +7 -0
  40. data/lib/axle_attributes/serializations/reflection.rb +40 -0
  41. data/lib/axle_attributes/serializations/serialization.rb +88 -0
  42. data/lib/axle_attributes/serializations/serialize_many.rb +88 -0
  43. data/lib/axle_attributes/serializations/serialize_one.rb +34 -0
  44. data/lib/axle_attributes/serialized_child.rb +58 -0
  45. data/lib/axle_attributes/validations.rb +29 -0
  46. data/lib/axle_attributes/versioned.rb +21 -0
  47. data/test/dummy/README.rdoc +6 -0
  48. data/test/dummy/Rakefile +6 -0
  49. data/test/dummy/app/assets/images/.keep +0 -0
  50. data/test/dummy/app/assets/javascripts/application.js +13 -0
  51. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  52. data/test/dummy/app/controllers/application_controller.rb +5 -0
  53. data/test/dummy/app/controllers/concerns/.keep +0 -0
  54. data/test/dummy/app/helpers/application_helper.rb +2 -0
  55. data/test/dummy/app/mailers/.keep +0 -0
  56. data/test/dummy/app/models/.keep +0 -0
  57. data/test/dummy/app/models/concerns/.keep +0 -0
  58. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  59. data/test/dummy/bin/bundle +3 -0
  60. data/test/dummy/bin/rails +4 -0
  61. data/test/dummy/bin/rake +4 -0
  62. data/test/dummy/config.ru +4 -0
  63. data/test/dummy/config/application.rb +12 -0
  64. data/test/dummy/config/boot.rb +5 -0
  65. data/test/dummy/config/database.yml +13 -0
  66. data/test/dummy/config/environment.rb +5 -0
  67. data/test/dummy/config/environments/development.rb +29 -0
  68. data/test/dummy/config/environments/production.rb +80 -0
  69. data/test/dummy/config/environments/test.rb +38 -0
  70. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  71. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  72. data/test/dummy/config/initializers/inflections.rb +16 -0
  73. data/test/dummy/config/initializers/mime_types.rb +5 -0
  74. data/test/dummy/config/initializers/secret_token.rb +12 -0
  75. data/test/dummy/config/initializers/session_store.rb +3 -0
  76. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  77. data/test/dummy/config/locales/en.yml +23 -0
  78. data/test/dummy/config/routes.rb +56 -0
  79. data/test/dummy/db/schema.rb +50 -0
  80. data/test/dummy/lib/assets/.keep +0 -0
  81. data/test/dummy/log/.keep +0 -0
  82. data/test/dummy/public/404.html +58 -0
  83. data/test/dummy/public/422.html +58 -0
  84. data/test/dummy/public/500.html +57 -0
  85. data/test/dummy/public/favicon.ico +0 -0
  86. data/test/factories/all.rb +14 -0
  87. data/test/helper.rb +25 -0
  88. data/test/lib/child_definition_test.rb +43 -0
  89. data/test/lib/conversions_test.rb +39 -0
  90. data/test/lib/definition/customization_test.rb +36 -0
  91. data/test/lib/definition/formatted_test.rb +75 -0
  92. data/test/lib/definition/indexed_test.rb +57 -0
  93. data/test/lib/definition/mappings_test.rb +26 -0
  94. data/test/lib/definition_test.rb +84 -0
  95. data/test/lib/dumper_test.rb +38 -0
  96. data/test/lib/format_test.rb +64 -0
  97. data/test/lib/has_attributes/json_reader_test.rb +46 -0
  98. data/test/lib/has_attributes_test.rb +69 -0
  99. data/test/lib/model_test.rb +44 -0
  100. data/test/lib/null_definition_test.rb +27 -0
  101. data/test/lib/parent_definition_test.rb +75 -0
  102. data/test/lib/provided_test.rb +46 -0
  103. data/test/lib/segmented_test.rb +27 -0
  104. data/test/lib/serializations/reflection_test.rb +24 -0
  105. data/test/lib/serializations/serialize_many_test.rb +194 -0
  106. data/test/lib/serializations/serialize_one_test.rb +122 -0
  107. data/test/lib/serializations_test.rb +24 -0
  108. data/test/lib/serialized_child_test.rb +91 -0
  109. data/test/lib/timestamp_attributes_test.rb +14 -0
  110. data/test/lib/validations_test.rb +8 -0
  111. data/test/lib/versioned_test.rb +35 -0
  112. data/test/models/attribute_customization/local_cache_test.rb +16 -0
  113. data/test/models/attribute_customization_test.rb +8 -0
  114. data/test/models/attribute_mapping/local_cache_test.rb +31 -0
  115. data/test/models/attribute_mapping_test.rb +20 -0
  116. data/test/support/business.rb +22 -0
  117. data/test/support/pg.rb +9 -0
  118. data/test/support/vegetable.rb +8 -0
  119. metadata +215 -0
@@ -0,0 +1,87 @@
1
+ module AxleAttributes
2
+ module HasAttributes
3
+ class JsonReader
4
+ # json = {
5
+ # {
6
+ # "location": {
7
+ # "street": {"type": "string"},
8
+ # "city": {"type": "string", "readonly": true}
9
+ # }
10
+ # "group2": {
11
+ # },
12
+ # ...
13
+ # },
14
+ # "contacts": {
15
+ # "type": "serialize_many",
16
+ # "attributes": {
17
+ # "first_name": {"type": "string"},
18
+ # "professional_title": {"type": "string"}
19
+ # }
20
+ # }
21
+ # }
22
+ #
23
+ # AxleAttributes::HasAttributes::JsonReader.read_definitions(Widget, json)
24
+ #
25
+ def self.read_definitions(model, json, options = {})
26
+ new(model, options).read_definitions(json)
27
+ end
28
+
29
+ attr_reader :model
30
+ attr_reader :reader_options
31
+ def initialize(model, reader_options = {})
32
+ @model = model
33
+ @reader_options = reader_options
34
+ end
35
+
36
+ def read_definitions(json)
37
+ json = ActiveSupport::JSON.decode(json) unless json.is_a?(Hash)
38
+
39
+ json.each do |namespace, options|
40
+ if options['type']
41
+ read_serialization namespace, options
42
+ else
43
+ read_definition_group options
44
+ end
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def read_definition_group(definitions)
51
+ definitions.each do |attribute_name, options|
52
+ model.has_attribute attribute_name, normalize_options(options)
53
+ end
54
+ end
55
+
56
+ SERIALIZATION_TYPES = %w(serialize_one serialize_many)
57
+ def read_serialization(serialization_name, options)
58
+ raise "unknown serialization #{options['type']}" unless SERIALIZATION_TYPES.include?(options['type'])
59
+ serialization_type = options['type']
60
+ serialization_attributes = options['attributes']
61
+
62
+ child_model = (options['class_name'] || serialization_name.classify).constantize
63
+ AxleAttributes::HasAttributes::JsonReader.read_definitions(child_model, {serialization_type => serialization_attributes}, reader_options)
64
+ reflection = model.send(serialization_type, serialization_name.to_sym, options.slice('class_name').symbolize_keys!)
65
+ end
66
+
67
+ SYMBOL_OPTIONS = [:type, :format, :index, :mmdb_name]
68
+ def normalize_options(options)
69
+ options.symbolize_keys!
70
+
71
+ SYMBOL_OPTIONS.each do |option_name|
72
+ if (value = options[option_name]).is_a?(String)
73
+ options[option_name] = value.to_sym
74
+ end
75
+ end
76
+
77
+ options.reverse_merge! default_options
78
+
79
+ options
80
+ end
81
+
82
+ def default_options
83
+ @default_options ||= (reader_options[:defaults] || {})
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,34 @@
1
+ module AxleAttributes
2
+ module Model
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ unless include?(ElasticRecord::Model)
7
+ include ElasticRecord::Model
8
+ end
9
+
10
+ include AxleAttributes::HasAttributes
11
+ self.has_attribute_definition_class = '::AxleAttributes::ParentDefinition'
12
+ add_core_axle_attributes! unless self.name.demodulize == 'Base'
13
+
14
+ include AxleAttributes::Provided
15
+ include AxleAttributes::Segmented
16
+ include AxleAttributes::Serializations
17
+ include AxleAttributes::Versioned
18
+ include AxleAttributes::Validations
19
+ end
20
+
21
+ module ClassMethods
22
+ def inherited(child)
23
+ super
24
+ child.add_core_axle_attributes!
25
+ end
26
+
27
+ def add_core_axle_attributes!
28
+ has_attribute(:created_at, type: :time, index: true, editable: false) unless attributes['created_at']
29
+ has_attribute(:updated_at, type: :time, index: true, editable: false) unless attributes['updated_at']
30
+ has_attribute(:segment_id, type: :integer, index: true, editable: false) unless attributes['segment_id']
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,11 @@
1
+ module AxleAttributes
2
+ class NullDefinition < ::AxleAttributes::Definition
3
+ def type
4
+ :null
5
+ end
6
+
7
+ def ==(other)
8
+ self.class == other.class && self.model == other.model && self.name == other.name
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,37 @@
1
+ module AxleAttributes
2
+ class ParentDefinition < ::AxleAttributes::Definition
3
+ setup do
4
+ setup_storage!
5
+ setup_index!
6
+ setup_versioning!
7
+ end
8
+
9
+ private
10
+
11
+ def setup_storage!
12
+ if type
13
+ storage_options = case type
14
+ when :text
15
+ {type: :string}
16
+ else
17
+ options.slice(:type, :default)
18
+ end
19
+
20
+ model.attribute(name, storage_options)
21
+ end
22
+ end
23
+
24
+ def setup_index!
25
+ if mapping = index_mapping
26
+ model.elastic_index.mapping[:properties].merge!(name => mapping)
27
+ end
28
+ end
29
+
30
+ def setup_versioning!
31
+ if versioned?
32
+ model.add_versioned_attribute name
33
+ end
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,67 @@
1
+ module AxleAttributes
2
+ module Provided
3
+ extend ActiveSupport::Concern
4
+ include ActiveModel::AttributeMethods
5
+
6
+ included do
7
+ attribute_method_suffix '_provided?'
8
+ end
9
+
10
+ module ClassMethods
11
+ def tracking_disabled?
12
+ Thread.current[:disable_provided_tracking]
13
+ end
14
+
15
+ def without_provided_tracking
16
+ Thread.current[:disable_provided_tracking] = true
17
+ yield
18
+ ensure
19
+ Thread.current[:disable_provided_tracking] = false
20
+ end
21
+ end
22
+
23
+ def provided
24
+ provided_set.to_a
25
+ end
26
+
27
+ def provided_set
28
+ @provided_set ||= Set.new
29
+ end
30
+
31
+ def provided?(attr)
32
+ provided_set.include? attr
33
+ end
34
+
35
+ def save(*)
36
+ if status = super
37
+ provided_set.clear
38
+ end
39
+ status
40
+ end
41
+
42
+ # Attempts to <tt>save!</tt> the record and clears changed attributes if successful.
43
+ def save!(*)
44
+ super.tap do
45
+ provided_set.clear
46
+ end
47
+ end
48
+
49
+ # <tt>reload</tt> the record and clears changed attributes.
50
+ def reload(*)
51
+ super.tap do
52
+ provided_set.clear
53
+ end
54
+ end
55
+
56
+ private
57
+ def attribute_provided?(attr)
58
+ provided_set.include?(attr)
59
+ end
60
+
61
+ def write_attribute(attr, value)
62
+ provided_set << attr.to_s unless self.class.tracking_disabled?
63
+
64
+ super(attr, value)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,4 @@
1
+ module AxleAttributes
2
+ class Regex
3
+ end
4
+ end
@@ -0,0 +1,30 @@
1
+ require 'digest'
2
+
3
+ module AxleAttributes
4
+ module Segmented
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ class_attribute :max_segment
9
+ self.max_segment = 1024
10
+
11
+ before_create do
12
+ regenerate_segment_id unless segment_id
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ def segment_range
18
+ 0..(max_segment-1)
19
+ end
20
+ end
21
+
22
+ def generate_segment_id
23
+ Digest::MD5.digest(id.to_s).unpack("L")[0] % max_segment
24
+ end
25
+
26
+ def regenerate_segment_id
27
+ self.segment_id = generate_segment_id
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,147 @@
1
+ module AxleAttributes
2
+ module Serializations
3
+ extend ActiveSupport::Concern
4
+ extend ActiveSupport::Autoload
5
+
6
+ autoload :Reflection, 'axle_attributes/serializations/reflection'
7
+ autoload :Serialization, 'axle_attributes/serializations/serialization'
8
+ autoload :SerializeMany, 'axle_attributes/serializations/serialize_many'
9
+ autoload :SerializeOne, 'axle_attributes/serializations/serialize_one'
10
+
11
+ module Builder
12
+ autoload :Serialization, 'axle_attributes/serializations/builder/serialization'
13
+ autoload :SerializeMany, 'axle_attributes/serializations/builder/serialize_many'
14
+ autoload :SerializeOne, 'axle_attributes/serializations/builder/serialize_one'
15
+ end
16
+
17
+ included do
18
+ class_attribute :serialization_reflections
19
+ self.serialization_reflections = {}
20
+
21
+ validate :validate_serialized_objects
22
+
23
+ after_save { child_serialization_cache.clear }
24
+ end
25
+
26
+ module ClassMethods
27
+ # serialize_many serializes a collection of objects into a single column.
28
+ #
29
+ # === Example
30
+ #
31
+ # class Factory
32
+ # serialize_many :widgets
33
+ # end
34
+ #
35
+ # This adds the following:
36
+ # <tt>factory.widgets_count</tt> - A counter cache of the number of widgets
37
+ # <tt>factory.widgets_updated_at</tt> - The last time a widget was modifed
38
+ # <tt>factory.widgets</tt> - A collection of widgets
39
+ #
40
+ # == Options
41
+ # [:class_name]
42
+ # The name of the class to instantiate the serialized objects as.
43
+ def serialize_many(name, options = {})
44
+ reflection = AxleAttributes::Serializations::Reflection.new(:serialize_many, name, options)
45
+ AxleAttributes::Serializations::Builder::SerializeMany.new(self, reflection).build!
46
+ reflection
47
+ end
48
+
49
+ # serialize_one serializes an object into a single column.
50
+ #
51
+ # === Example
52
+ #
53
+ # class Factory
54
+ # serialize_one :manager
55
+ # end
56
+ #
57
+ # This adds the following:
58
+ # <tt>factory.manager</tt> - Read the manager.
59
+ # <tt>factory.manager=(value)</tt> - Write the manager.
60
+ #
61
+ # == Options
62
+ # [:class_name]
63
+ # The name of the class to instantiate the serialized objects as.
64
+ def serialize_one(name, options = {})
65
+ reflection = AxleAttributes::Serializations::Reflection.new(:serialize_one, name, options)
66
+ AxleAttributes::Serializations::Builder::SerializeOne.new(self, reflection).build!
67
+ reflection
68
+ end
69
+
70
+ def serialized_many?(name)
71
+ serialization_reflections[name.to_sym].try :collection?
72
+ end
73
+
74
+ def generated_serialized_object_methods
75
+ @generated_serialized_object_methods ||= begin
76
+ mod = const_set(:GeneratedSerializedObjectMethods, Module.new)
77
+ include mod
78
+ mod
79
+ end
80
+ end
81
+ end
82
+
83
+ # Returns the searches_many instance for the given name, instantiating it if it doesn't already exist
84
+ def child_serialization(name)
85
+ serialization = child_serialization_instance_get(name)
86
+
87
+ if serialization.nil?
88
+ reflection = serialization_reflections[name]
89
+ serialization = reflection.serialization_class.new(self, reflection)
90
+ child_serialization_instance_set(name, serialization)
91
+ end
92
+
93
+ serialization
94
+ end
95
+
96
+ def reload
97
+ super
98
+ child_serialization_cache.clear
99
+ end
100
+
101
+ def validate_serialized_objects
102
+ child_serialization_cache.each do |name, serialization|
103
+ next unless serialization.loaded?
104
+
105
+ validation_method = serialization.reflection.collection? ? :validate_collection_serialization : :validate_single_serialization
106
+ send(validation_method, serialization)
107
+ end
108
+ end
109
+
110
+ def validate_collection_serialization(serialization)
111
+ serialization.reader.each do |record|
112
+ if record.invalid?
113
+ record.errors.each do |attribute, message|
114
+ attribute = "#{serialization.name}.#{attribute}"
115
+ errors[attribute] << message
116
+ errors[attribute].uniq!
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ def validate_single_serialization(serialization)
123
+ record = serialization.reader
124
+
125
+ if record && record.invalid?
126
+ record.errors.each do |attribute, message|
127
+ attribute = "#{serialization.name}.#{attribute}"
128
+ errors[attribute] << message
129
+ end
130
+ end
131
+ end
132
+
133
+ private
134
+
135
+ def child_serialization_cache
136
+ @child_serialization_cache ||= {}
137
+ end
138
+
139
+ def child_serialization_instance_get(name)
140
+ child_serialization_cache[name.to_sym]
141
+ end
142
+
143
+ def child_serialization_instance_set(name, serialization)
144
+ child_serialization_cache[name.to_sym] = serialization
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,83 @@
1
+ module AxleAttributes::Serializations::Builder
2
+ class Serialization
3
+ attr_accessor :model, :reflection
4
+ def initialize(model, reflection)
5
+ @model = model
6
+ @reflection = reflection
7
+ end
8
+
9
+ def build!
10
+ model.serialization_reflections = model.serialization_reflections.merge(reflection.name => reflection)
11
+
12
+ define_attributes
13
+ define_writer
14
+ define_reader
15
+ end
16
+
17
+ def define_attributes
18
+ model.attribute_definition_class.new(model, reflection.storage_column, type: :json, index: false, version: true).setup!
19
+
20
+ model.has_attributes(meta_attributes)
21
+
22
+ setup_definitions_on_parent
23
+ end
24
+
25
+ def mixin
26
+ model.generated_serialized_object_methods
27
+ end
28
+
29
+ def define_writer
30
+ name = reflection.name
31
+
32
+ mixin.redefine_method "#{name}=" do |records|
33
+ child_serialization(name).writer(records)
34
+ end
35
+
36
+ mixin.redefine_method "#{name}_attributes=" do |attrs|
37
+ child_serialization(name).attributes_writer(attrs)
38
+ end
39
+ end
40
+
41
+ def define_reader
42
+ name = reflection.name
43
+
44
+ mixin.redefine_method(name) do
45
+ child_serialization(name).reader
46
+ end
47
+
48
+ mixin.redefine_method("#{name}_was") do
49
+ child_serialization(name).reader_was
50
+ end
51
+ end
52
+
53
+ def setup_definitions_on_parent
54
+ serialized_index_mapping = {
55
+ type: index_type,
56
+ properties: {}
57
+ }
58
+
59
+ reflection.klass.attributes.each_value do |definition|
60
+ definition.nested_path = reflection.name.to_s if reflection.collection?
61
+
62
+ model.add_attribute_definition("#{reflection.name}.#{definition.name}", definition)
63
+
64
+ if mapping = definition.index_mapping
65
+ serialized_index_mapping[:properties][definition.name] = mapping
66
+ end
67
+ end
68
+
69
+ model.elastic_index.mapping[:properties].merge!(reflection.name.to_s => serialized_index_mapping)
70
+ end
71
+
72
+ protected
73
+
74
+ def index_type
75
+ raise "index_type must be implemented"
76
+ end
77
+
78
+ def meta_attributes
79
+ {}
80
+ end
81
+
82
+ end
83
+ end