axle_attributes 1.13.2

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