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,34 @@
1
+ module AxleAttributes
2
+ class Definition
3
+ # Adds support for +setup+ and +teardown+ callbacks.
4
+ # These callbacks serve as a replacement to overwriting the
5
+ # <tt>#setup</tt> and <tt>#teardown</tt> methods of your TestCase.
6
+ #
7
+ # class ExampleTest < ActiveSupport::TestCase
8
+ # setup do
9
+ # # ...
10
+ # end
11
+ # end
12
+ module Callbacks
13
+
14
+ extend ActiveSupport::Concern
15
+
16
+ included do
17
+ include ActiveSupport::Callbacks
18
+ define_callbacks :setup
19
+ end
20
+
21
+ module ClassMethods
22
+ # Add a callback, which runs before <tt>TestCase#setup</tt>.
23
+ def setup(*args, &block)
24
+ set_callback(:setup, :before, *args, &block)
25
+ end
26
+ end
27
+
28
+ def before_setup # :nodoc:
29
+ super
30
+ run_callbacks :setup
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,51 @@
1
+ module AxleAttributes
2
+ class Definition
3
+ module Customization
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class_attribute :cached_customizations
8
+ self.cached_customizations = {}
9
+ end
10
+
11
+ module ClassMethods
12
+ def cached_customization_for(namespace)
13
+ cached_customizations[namespace] ||= AttributeCustomization.namespace_cache_for(namespace).read
14
+ end
15
+ end
16
+
17
+ def customization
18
+ self.class.cached_customization_for(namespace)[name]
19
+ end
20
+
21
+ def display_name
22
+ read_customization(:display_name) || name.titlecase.gsub('.', ' ')
23
+ end
24
+
25
+ def description
26
+ read_customization :description
27
+ end
28
+
29
+ def notes
30
+ read_customization :notes
31
+ end
32
+
33
+ def editable_customization
34
+ value = customization.try(:editable)
35
+ value.nil? ? true : value
36
+ end
37
+
38
+ def deprecated_on
39
+ read_customization :deprecated_on
40
+ end
41
+
42
+ def deprecated?
43
+ deprecated_on.present?
44
+ end
45
+
46
+ def read_customization(property)
47
+ customization.try(property).presence
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,57 @@
1
+ module AxleAttributes
2
+ class Definition
3
+ module Formatted
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class_attribute :formatters
8
+ self.formatters = {}
9
+
10
+ setup do
11
+ setup_method_override!
12
+ formatter.try(:setup!, self)
13
+ end
14
+ end
15
+
16
+ def analyzer_mapping
17
+ if mapping = formatter.try(:analyzer_mapping)
18
+ mapping
19
+ else
20
+ super
21
+ end
22
+ end
23
+
24
+ def instructions
25
+ fetch_format_property(:instructions)
26
+ end
27
+
28
+ def standardize(value)
29
+ formatter.try(:standardize, self, value) || value
30
+ end
31
+
32
+ def validate(value)
33
+ formatter.try :validate, self, value
34
+ end
35
+
36
+ def formatter
37
+ formatters[format]
38
+ end
39
+
40
+ private
41
+
42
+ def fetch_format_property(property)
43
+ read_customization(property) || formatter.try(property, self)
44
+ end
45
+
46
+ def setup_method_override!
47
+ if formatter.respond_to?(:standardize)
48
+ definition = self
49
+ model.send :define_method, "#{name}=" do |value|
50
+ super definition.standardize(value)
51
+ end
52
+ end
53
+ end
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,41 @@
1
+ module AxleAttributes
2
+ class Definition
3
+ module Indexed
4
+ def index
5
+ options[:index]
6
+ end
7
+
8
+ ANALYZERS = [:snowball, :proper_noun, :simple]
9
+ def analyzer_mapping
10
+ {type: 'string', analyzer: index} if ANALYZERS.include?(index)
11
+ end
12
+
13
+ INDEXES_BY_TYPE = {
14
+ array: {type: "string", index: "not_analyzed", doc_values: true},
15
+ boolean: {type: "boolean", doc_values: true},
16
+ float: {type: "double", doc_values: true},
17
+ integer: {type: "long", doc_values: true},
18
+ json: {type: "boolean"},
19
+ string: {type: 'string', index_analyzer: 'keyword', search_analyzer: 'standard'},
20
+ text: {type: 'string', index_analyzer: 'keyword', search_analyzer: 'standard'},
21
+ time: {type: "date", format: "dateOptionalTime", doc_values: true},
22
+ date: {type: "date", format: "date", doc_values: true}
23
+ }
24
+ def index_mapping
25
+ return unless index
26
+
27
+ if mapping = analyzer_mapping
28
+ multi_field_index(name, mapping)
29
+ elsif index == true
30
+ INDEXES_BY_TYPE[type] || (raise "unknown index type #{type.inspect} for #{name}")
31
+ elsif index.is_a?(Hash)
32
+ index
33
+ end
34
+ end
35
+
36
+ def multi_field_index(field, analyzer_mapping)
37
+ {type: "multi_field", fields: {field => {type: "string", index: "not_analyzed", doc_values: true}, analyzed: analyzer_mapping}}
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,48 @@
1
+ module AxleAttributes
2
+ class Definition
3
+ module Mappings
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class_attribute :cached_attribute_mappings
8
+ self.cached_attribute_mappings = {}
9
+ end
10
+
11
+ module ClassMethods
12
+ def cached_attribute_mappings_for(namespace)
13
+ cached_attribute_mappings[namespace] ||= AttributeMapping.cached_mapping_for(namespace).read
14
+ end
15
+ end
16
+
17
+ def to_options
18
+ mappings.map do |k, v|
19
+ [v, k]
20
+ end
21
+ end
22
+
23
+ def mappings
24
+ if @mappings
25
+ @mappings
26
+ elsif mapping_model && mapping_model.respond_to?(:cached_mapping)
27
+ mapping_model.cached_mapping
28
+ else
29
+ self.class.cached_attribute_mappings_for(namespace)[name] || {}
30
+ end
31
+ end
32
+
33
+ def mappings=(val)
34
+ @mappings = val
35
+ end
36
+
37
+ def mapping_records
38
+ AttributeMapping.where(namespace: namespace, attribute_name: name)
39
+ end
40
+
41
+ def mapping_model
42
+ if options[:mapping_model]
43
+ @mapping_model ||= options[:mapping_model].constantize
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,54 @@
1
+ require 'csv'
2
+
3
+ module AxleAttributes
4
+ class Dumper
5
+ attr_reader :model
6
+ def initialize(model)
7
+ @model = model
8
+ end
9
+
10
+ def dump_internal
11
+ dump(model.attribute_set, ["Attribute", "Display Name", "Type", "Deprecation", "Legacy Name", "Mapping", "Description", "Note"]) do |definition|
12
+ [
13
+ definition.canonical_name,
14
+ definition.display_name,
15
+ definition.type,
16
+ definition.deprecated_on ? definition.deprecated_on.to_s('%Y/%m/%d') : nil,
17
+ definition.options[:mmdb_name],
18
+ mapping_display(definition),
19
+ definition.description,
20
+ definition.notes
21
+ ]
22
+ end
23
+ end
24
+
25
+ def dump_public(attribute_set)
26
+ dump(attribute_set, ["Attribute", "Display Name", "Type", "Description"]) do |definition|
27
+ [
28
+ definition.canonical_name,
29
+ definition.display_name,
30
+ definition.type,
31
+ definition.description
32
+ ]
33
+ end
34
+ end
35
+
36
+ def dump(attribute_set, headers)
37
+ CSV.generate(headers: headers, write_headers: true) do |csv|
38
+ attribute_set.sort.each do |attribute_name|
39
+ definition = model.attributes[attribute_name]
40
+ csv << yield(definition) if definition
41
+ end
42
+ end
43
+ end
44
+
45
+ def mapping_display(definition)
46
+ if mapping_model = definition.mapping_model
47
+ "#{mapping_model.model_name.plural}.#{mapping_model.natural_key}" if mapping_model.respond_to?(:natural_key)
48
+ elsif definition.mappings.any?
49
+ "attribute_mappings"
50
+ end
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,4 @@
1
+ module AxleAttributes
2
+ class Engine < ::Rails::Engine
3
+ end
4
+ end
@@ -0,0 +1,74 @@
1
+ module AxleAttributes
2
+ class Format
3
+ class_attribute :phone_regex
4
+ self.phone_regex = /\A\(\d{3}\) \d{3}-\d{4}\z/
5
+
6
+ class_attribute :url_regex
7
+ self.url_regex = /\A(https?:\/\/)([-a-z0-9]+\.[-.a-z0-9:]+)(\/[^\s]+)?\z/
8
+
9
+ class_attribute :zip_regex
10
+ self.zip_regex = /\A\d{5}\z/
11
+
12
+ class_attribute :postal_code_regex
13
+ self.postal_code_regex = /\A(\d{5}(-\d{4})?|[A-Z0-9]{6})\z/
14
+
15
+ class << self
16
+ PHONEWORDS = {"A"=>"2", "B"=>"2", "C"=>"2", "D"=>"3", "E"=>"3", "F"=>"3", "G"=>"4", "H"=>"4", "I"=>"4", "J"=>"5",
17
+ "K"=>"5", "L"=>"5", "M"=>"6", "N"=>"6", "O"=>"6", "P"=>"7", "Q"=>"7", "R"=>"7", "S"=>"7", "T"=>"8",
18
+ "U"=>"8", "V"=>"8", "W"=>"9", "X"=>"9", "Y"=>"9", "Z"=>"9"}
19
+
20
+ def standardize_phone(phone)
21
+ standardize phone, self.phone_regex do
22
+ standardized = phone.chars.map { |char| PHONEWORDS[char.upcase] || char }.join('')
23
+
24
+ standardized.gsub!(/\D/, '')
25
+
26
+ if standardized.size == 10
27
+ standardized.gsub!(/(\d{3})(\d{3})(\d{4})/, "(\\1) \\2-\\3")
28
+ standardized
29
+ elsif standardized.size == 11 && standardized[0] == '1'
30
+ standardized.gsub!(/1(\d{3})(\d{3})(\d{4})/, "(\\1) \\2-\\3")
31
+ standardized
32
+ end
33
+ end
34
+ end
35
+
36
+ def standardize_postal_code(postal_code)
37
+ standardize postal_code, postal_code_regex do
38
+ standardized = postal_code.remove(/[^\-[:alnum:]]/)
39
+ case standardized.length
40
+ when 6 then standardized.upcase!
41
+ when 9 then standardized.sub!(/\A(\d{5})(\d{4})\z/, '\\1-\\2')
42
+ end
43
+
44
+ standardized
45
+ end
46
+ end
47
+
48
+ def standardize_url(website)
49
+ standardize website, self.url_regex do
50
+ if website =~ /^(\w*):\/\//
51
+ scheme, _, resource = website.partition("://")
52
+ scheme.downcase!
53
+ else
54
+ scheme = "http"
55
+ resource = website
56
+ end
57
+ resource.strip!
58
+
59
+ host, slash, path = resource.partition("/")
60
+ host.downcase!
61
+ path.gsub! ' ', '%20'
62
+
63
+ path.empty? ? "#{scheme}://#{host}" : "#{scheme}://#{host}/#{path}"
64
+ end
65
+ end
66
+
67
+ def standardize(value, regex)
68
+ return value if value.blank? || value =~ regex
69
+ standardized = yield(value)
70
+ standardized =~ regex ? standardized : value
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,104 @@
1
+ require 'axle_attributes/has_attributes/json_reader'
2
+ require 'open-uri'
3
+
4
+ module AxleAttributes
5
+ module HasAttributes
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :has_attribute_definition_class
10
+ self.has_attribute_definition_class = '::AxleAttributes::Definition'
11
+ end
12
+
13
+ module ClassMethods
14
+ def inherited(child)
15
+ child.attribute_set = attribute_set.dup
16
+ child.attributes = attributes.dup
17
+ super
18
+ end
19
+
20
+ # Example:
21
+ # has_attributes(
22
+ # color: {type: :string, index: true, version: true},
23
+ # weight: {type: :integer, version: true}
24
+ # )
25
+ def has_attributes(definitions, default_options = {})
26
+ definitions.each do |name, options|
27
+ has_attribute name, default_options.merge(options)
28
+ end
29
+ end
30
+
31
+ def has_attribute(name, options = {})
32
+ name = name.to_s
33
+ definition = attribute_definition_class.new(self, name, options)
34
+ add_attribute_definition(name, definition)
35
+ definition.setup!
36
+ end
37
+
38
+ def add_attribute_definition(name, definition)
39
+ attributes[name] = definition
40
+ attribute_set << name
41
+ end
42
+
43
+ def has_mappings(mappings_hash)
44
+ mappings_hash.each do |name, mappings|
45
+ has_mapping name, mappings
46
+ end
47
+ end
48
+
49
+ def has_mapping(name, mappings)
50
+ if mappings.is_a?(Array)
51
+ mappings = Hash[mappings.map { |v| [v, v.humanize] }]
52
+ end
53
+
54
+ attributes[name.to_s].mappings = mappings
55
+ end
56
+
57
+ def attributes_file_root
58
+ @attributes_file_root ||= Rails.root.join('db', 'attributes')
59
+ end
60
+
61
+ def attributes_file_root=(pathname)
62
+ pathname = Pathname.new(pathname) unless pathname.is_a?(Pathname)
63
+ @attributes_file_root = pathname
64
+ end
65
+
66
+ def has_attributes_file(filename, options = {})
67
+ options[:defaults] ||= {}
68
+ options[:defaults][:filename] = filename
69
+ AxleAttributes::HasAttributes::JsonReader.read_definitions(self, read_attributes_file(filename), options)
70
+ end
71
+
72
+ def attributes
73
+ @attributes ||= {}
74
+ end
75
+
76
+ def attributes=(attributes)
77
+ @attributes = attributes
78
+ end
79
+
80
+ def attribute_set
81
+ @attribute_set ||= Set.new
82
+ end
83
+
84
+ def attribute_set=(attribute_set)
85
+ @attribute_set = attribute_set
86
+ end
87
+
88
+ def attribute_definition_class
89
+ @attribute_definition_class ||= has_attribute_definition_class.constantize
90
+ end
91
+
92
+ private
93
+
94
+ def read_attributes_file(filename)
95
+ attributes_file_pathname(filename).read
96
+ end
97
+
98
+ def attributes_file_pathname(filename)
99
+ attributes_file_root.join("#{filename}.json")
100
+ end
101
+
102
+ end
103
+ end
104
+ end