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