shopify_product_taxonomy 1.0.0

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 (36) hide show
  1. checksums.yaml +7 -0
  2. data/lib/product_taxonomy/alphanumeric_sorter.rb +113 -0
  3. data/lib/product_taxonomy/identifier_formatter.rb +28 -0
  4. data/lib/product_taxonomy/loader.rb +31 -0
  5. data/lib/product_taxonomy/localizations_validator.rb +31 -0
  6. data/lib/product_taxonomy/models/attribute.rb +170 -0
  7. data/lib/product_taxonomy/models/category.rb +313 -0
  8. data/lib/product_taxonomy/models/extended_attribute.rb +47 -0
  9. data/lib/product_taxonomy/models/integration_version.rb +296 -0
  10. data/lib/product_taxonomy/models/mapping_rule.rb +114 -0
  11. data/lib/product_taxonomy/models/mixins/formatted_validation_errors.rb +23 -0
  12. data/lib/product_taxonomy/models/mixins/indexed.rb +133 -0
  13. data/lib/product_taxonomy/models/mixins/localized.rb +70 -0
  14. data/lib/product_taxonomy/models/serializers/attribute/data/data_serializer.rb +48 -0
  15. data/lib/product_taxonomy/models/serializers/attribute/data/localizations_serializer.rb +34 -0
  16. data/lib/product_taxonomy/models/serializers/attribute/dist/json_serializer.rb +41 -0
  17. data/lib/product_taxonomy/models/serializers/attribute/dist/txt_serializer.rb +42 -0
  18. data/lib/product_taxonomy/models/serializers/attribute/docs/base_and_extended_serializer.rb +41 -0
  19. data/lib/product_taxonomy/models/serializers/attribute/docs/reversed_serializer.rb +55 -0
  20. data/lib/product_taxonomy/models/serializers/attribute/docs/search_serializer.rb +32 -0
  21. data/lib/product_taxonomy/models/serializers/category/data/data_serializer.rb +29 -0
  22. data/lib/product_taxonomy/models/serializers/category/data/full_names_serializer.rb +26 -0
  23. data/lib/product_taxonomy/models/serializers/category/data/localizations_serializer.rb +34 -0
  24. data/lib/product_taxonomy/models/serializers/category/dist/json_serializer.rb +60 -0
  25. data/lib/product_taxonomy/models/serializers/category/dist/txt_serializer.rb +42 -0
  26. data/lib/product_taxonomy/models/serializers/category/docs/search_serializer.rb +33 -0
  27. data/lib/product_taxonomy/models/serializers/category/docs/siblings_serializer.rb +39 -0
  28. data/lib/product_taxonomy/models/serializers/value/data/data_serializer.rb +28 -0
  29. data/lib/product_taxonomy/models/serializers/value/data/localizations_serializer.rb +34 -0
  30. data/lib/product_taxonomy/models/serializers/value/dist/json_serializer.rb +31 -0
  31. data/lib/product_taxonomy/models/serializers/value/dist/txt_serializer.rb +38 -0
  32. data/lib/product_taxonomy/models/taxonomy.rb +11 -0
  33. data/lib/product_taxonomy/models/value.rb +147 -0
  34. data/lib/product_taxonomy/version.rb +6 -0
  35. data/lib/product_taxonomy.rb +50 -0
  36. metadata +124 -0
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductTaxonomy
4
+ module Serializers
5
+ module Value
6
+ module Dist
7
+ class TxtSerializer
8
+ class << self
9
+ def serialize_all(version:, locale: "en", padding: longest_gid_length)
10
+ header = <<~HEADER
11
+ # Shopify Product Taxonomy - Attribute Values: #{version}
12
+ # Format: {GID} : {Value name} [{Attribute name}]
13
+
14
+ HEADER
15
+
16
+ header + ProductTaxonomy::Value.all_values_sorted.map { serialize(_1, padding:, locale:) }.join("\n")
17
+ end
18
+
19
+ # @param value [Value]
20
+ # @param padding [Integer] The padding to use for the GID.
21
+ # @param locale [String] The locale to use for localization.
22
+ # @return [String]
23
+ def serialize(value, padding: 0, locale: "en")
24
+ "#{value.gid.ljust(padding)} : #{value.full_name(locale:)}"
25
+ end
26
+
27
+ private
28
+
29
+ def longest_gid_length
30
+ largest_id = ProductTaxonomy::Value.hashed_by(:id).keys.max
31
+ ProductTaxonomy::Value.find_by(id: largest_id).gid.length
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductTaxonomy
4
+ class Taxonomy
5
+ attr_reader :verticals
6
+
7
+ def initialize(verticals:)
8
+ @verticals = verticals
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductTaxonomy
4
+ # An attribute value in the product taxonomy. Referenced by a {ProductTaxonomy::Attribute}. For example, an
5
+ # attribute called "Color" could have values "Red", "Blue", and "Green".
6
+ class Value
7
+ include ActiveModel::Validations
8
+ include FormattedValidationErrors
9
+ extend Localized
10
+ extend Indexed
11
+
12
+ class << self
13
+ # Load values from source data. By default, this data is deserialized from a YAML file in the `data` directory.
14
+ #
15
+ # @param source_data [Array<Hash>] The source data to load values from.
16
+ def load_from_source(source_data)
17
+ raise ArgumentError, "source_data must be an array" unless source_data.is_a?(Array)
18
+
19
+ source_data.each do |value_data|
20
+ raise ArgumentError, "source_data must contain hashes" unless value_data.is_a?(Hash)
21
+
22
+ Value.create_validate_and_add!(
23
+ id: value_data["id"],
24
+ name: value_data["name"],
25
+ friendly_id: value_data["friendly_id"],
26
+ handle: value_data["handle"],
27
+ )
28
+ end
29
+ end
30
+
31
+ # Reset all class-level state
32
+ def reset
33
+ @localizations = nil
34
+ @hashed_models = nil
35
+ end
36
+
37
+ # Sort values by their localized name.
38
+ #
39
+ # @param values [Array<Value>] The values to sort.
40
+ # @param locale [String] The locale to sort by.
41
+ # @return [Array<Value>] The sorted values.
42
+ def sort_by_localized_name(values, locale: "en")
43
+ values.sort_by.with_index do |value, idx|
44
+ [
45
+ value.name(locale: "en").downcase == "other" ? 1 : 0,
46
+ *AlphanumericSorter.normalize_value(value.name(locale:)),
47
+ idx,
48
+ ]
49
+ end
50
+ end
51
+
52
+ # Sort values according to their English name, taking "other" into account.
53
+ #
54
+ # @param values [Array<Value>] The values to sort.
55
+ # @return [Array<Value>] The sorted values.
56
+ def all_values_sorted
57
+ all.sort_by do |value|
58
+ [
59
+ value.name(locale: "en").downcase == "other" ? 1 : 0,
60
+ value.name(locale: "en"),
61
+ value.id,
62
+ ]
63
+ end
64
+ end
65
+
66
+ # Get the next ID for a newly created value.
67
+ #
68
+ # @return [Integer] The next ID.
69
+ def next_id = (all.max_by(&:id)&.id || 0) + 1
70
+ end
71
+
72
+ # Validations that run when the value is created
73
+ validates :id, presence: true, numericality: { only_integer: true }, on: :create
74
+ validates :name, presence: true, on: :create
75
+ validates :friendly_id, presence: true, on: :create
76
+ validates :handle, presence: true, on: :create
77
+ validates_with ProductTaxonomy::Indexed::UniquenessValidator, attributes: [:friendly_id, :handle, :id], on: :create
78
+
79
+ # Validations that run when the taxonomy has been loaded
80
+ validate :friendly_id_prefix_resolves_to_attribute?, on: :taxonomy_loaded
81
+ validate :handle_prefix_matches_attribute?, on: :taxonomy_loaded
82
+
83
+ localized_attr_reader :name
84
+
85
+ attr_reader :id, :friendly_id, :handle
86
+
87
+ # @param id [Integer] The ID of the value.
88
+ # @param name [String] The name of the value.
89
+ # @param friendly_id [String] The friendly ID of the value.
90
+ # @param handle [String] The handle of the value.
91
+ def initialize(id:, name:, friendly_id:, handle:)
92
+ @id = id
93
+ @name = name
94
+ @friendly_id = friendly_id
95
+ @handle = handle
96
+ end
97
+
98
+ def gid
99
+ "gid://shopify/TaxonomyValue/#{id}"
100
+ end
101
+
102
+ # Get the primary attribute for this value.
103
+ #
104
+ # @return [ProductTaxonomy::Attribute] The primary attribute for this value.
105
+ def primary_attribute
106
+ @primary_attribute ||= Attribute.find_by(friendly_id: primary_attribute_friendly_id)
107
+ end
108
+
109
+ # Get the full name of the value, including the primary attribute.
110
+ #
111
+ # @param locale [String] The locale to get the name in.
112
+ # @return [String] The full name of the value.
113
+ def full_name(locale: "en")
114
+ "#{name(locale:)} [#{primary_attribute.name(locale:)}]"
115
+ end
116
+
117
+ private
118
+
119
+ def primary_attribute_friendly_id = friendly_id.split("__").first
120
+
121
+ #
122
+ # Validation
123
+ #
124
+ def friendly_id_prefix_resolves_to_attribute?
125
+ return if primary_attribute
126
+
127
+ errors.add(
128
+ :friendly_id,
129
+ :invalid_prefix,
130
+ message: "prefix \"#{primary_attribute_friendly_id}\" does not match the friendly_id of any attribute",
131
+ )
132
+ end
133
+
134
+ def handle_prefix_matches_attribute?
135
+ return if primary_attribute.nil?
136
+
137
+ handle_prefix = handle.split("__").first
138
+ return if primary_attribute && primary_attribute.handle == handle_prefix
139
+
140
+ errors.add(
141
+ :handle,
142
+ :invalid_prefix,
143
+ message: "prefix \"#{handle_prefix}\" does not match primary attribute handle \"#{primary_attribute.handle}\"",
144
+ )
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductTaxonomy
4
+ VERSION = "1.0.0"
5
+ end
6
+
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/all"
4
+ require "active_model"
5
+
6
+ module ProductTaxonomy
7
+ DEFAULT_DATA_PATH = File.expand_path("../../data", __dir__)
8
+
9
+ class << self
10
+ attr_writer :data_path
11
+
12
+ def data_path
13
+ @data_path ||= DEFAULT_DATA_PATH
14
+ end
15
+ end
16
+ end
17
+
18
+ require_relative "product_taxonomy/version"
19
+ require_relative "product_taxonomy/loader"
20
+ require_relative "product_taxonomy/alphanumeric_sorter"
21
+ require_relative "product_taxonomy/identifier_formatter"
22
+ require_relative "product_taxonomy/localizations_validator"
23
+ require_relative "product_taxonomy/models/mixins/localized"
24
+ require_relative "product_taxonomy/models/mixins/indexed"
25
+ require_relative "product_taxonomy/models/mixins/formatted_validation_errors"
26
+ require_relative "product_taxonomy/models/attribute"
27
+ require_relative "product_taxonomy/models/extended_attribute"
28
+ require_relative "product_taxonomy/models/value"
29
+ require_relative "product_taxonomy/models/category"
30
+ require_relative "product_taxonomy/models/taxonomy"
31
+ require_relative "product_taxonomy/models/mapping_rule"
32
+ require_relative "product_taxonomy/models/integration_version"
33
+ require_relative "product_taxonomy/models/serializers/category/data/data_serializer"
34
+ require_relative "product_taxonomy/models/serializers/category/data/localizations_serializer"
35
+ require_relative "product_taxonomy/models/serializers/category/data/full_names_serializer"
36
+ require_relative "product_taxonomy/models/serializers/category/docs/siblings_serializer"
37
+ require_relative "product_taxonomy/models/serializers/category/docs/search_serializer"
38
+ require_relative "product_taxonomy/models/serializers/category/dist/json_serializer"
39
+ require_relative "product_taxonomy/models/serializers/category/dist/txt_serializer"
40
+ require_relative "product_taxonomy/models/serializers/attribute/data/data_serializer"
41
+ require_relative "product_taxonomy/models/serializers/attribute/data/localizations_serializer"
42
+ require_relative "product_taxonomy/models/serializers/attribute/docs/base_and_extended_serializer"
43
+ require_relative "product_taxonomy/models/serializers/attribute/docs/reversed_serializer"
44
+ require_relative "product_taxonomy/models/serializers/attribute/docs/search_serializer"
45
+ require_relative "product_taxonomy/models/serializers/attribute/dist/json_serializer"
46
+ require_relative "product_taxonomy/models/serializers/attribute/dist/txt_serializer"
47
+ require_relative "product_taxonomy/models/serializers/value/data/data_serializer"
48
+ require_relative "product_taxonomy/models/serializers/value/data/localizations_serializer"
49
+ require_relative "product_taxonomy/models/serializers/value/dist/json_serializer"
50
+ require_relative "product_taxonomy/models/serializers/value/dist/txt_serializer"
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shopify_product_taxonomy
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Shopify
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-11-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activemodel
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '7.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '7.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: json
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 2.16.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 2.16.0
55
+ description: A code-only gem providing Ruby classes and utilities to parse and process
56
+ Shopify's Standard Product Taxonomy data from YAML source files into an in-memory
57
+ Ruby object model. Data files are available at https://github.com/Shopify/product-taxonomy
58
+ email:
59
+ - gems@shopify.com
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - lib/product_taxonomy.rb
65
+ - lib/product_taxonomy/alphanumeric_sorter.rb
66
+ - lib/product_taxonomy/identifier_formatter.rb
67
+ - lib/product_taxonomy/loader.rb
68
+ - lib/product_taxonomy/localizations_validator.rb
69
+ - lib/product_taxonomy/models/attribute.rb
70
+ - lib/product_taxonomy/models/category.rb
71
+ - lib/product_taxonomy/models/extended_attribute.rb
72
+ - lib/product_taxonomy/models/integration_version.rb
73
+ - lib/product_taxonomy/models/mapping_rule.rb
74
+ - lib/product_taxonomy/models/mixins/formatted_validation_errors.rb
75
+ - lib/product_taxonomy/models/mixins/indexed.rb
76
+ - lib/product_taxonomy/models/mixins/localized.rb
77
+ - lib/product_taxonomy/models/serializers/attribute/data/data_serializer.rb
78
+ - lib/product_taxonomy/models/serializers/attribute/data/localizations_serializer.rb
79
+ - lib/product_taxonomy/models/serializers/attribute/dist/json_serializer.rb
80
+ - lib/product_taxonomy/models/serializers/attribute/dist/txt_serializer.rb
81
+ - lib/product_taxonomy/models/serializers/attribute/docs/base_and_extended_serializer.rb
82
+ - lib/product_taxonomy/models/serializers/attribute/docs/reversed_serializer.rb
83
+ - lib/product_taxonomy/models/serializers/attribute/docs/search_serializer.rb
84
+ - lib/product_taxonomy/models/serializers/category/data/data_serializer.rb
85
+ - lib/product_taxonomy/models/serializers/category/data/full_names_serializer.rb
86
+ - lib/product_taxonomy/models/serializers/category/data/localizations_serializer.rb
87
+ - lib/product_taxonomy/models/serializers/category/dist/json_serializer.rb
88
+ - lib/product_taxonomy/models/serializers/category/dist/txt_serializer.rb
89
+ - lib/product_taxonomy/models/serializers/category/docs/search_serializer.rb
90
+ - lib/product_taxonomy/models/serializers/category/docs/siblings_serializer.rb
91
+ - lib/product_taxonomy/models/serializers/value/data/data_serializer.rb
92
+ - lib/product_taxonomy/models/serializers/value/data/localizations_serializer.rb
93
+ - lib/product_taxonomy/models/serializers/value/dist/json_serializer.rb
94
+ - lib/product_taxonomy/models/serializers/value/dist/txt_serializer.rb
95
+ - lib/product_taxonomy/models/taxonomy.rb
96
+ - lib/product_taxonomy/models/value.rb
97
+ - lib/product_taxonomy/version.rb
98
+ homepage: https://github.com/Shopify/product-taxonomy
99
+ licenses:
100
+ - MIT
101
+ metadata:
102
+ bug_tracker_uri: https://github.com/Shopify/product-taxonomy/issues
103
+ source_code_uri: https://github.com/Shopify/product-taxonomy
104
+ allowed_push_host: https://rubygems.org
105
+ post_install_message:
106
+ rdoc_options: []
107
+ require_paths:
108
+ - lib
109
+ required_ruby_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '3.0'
114
+ required_rubygems_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ requirements: []
120
+ rubygems_version: 3.5.3
121
+ signing_key:
122
+ specification_version: 4
123
+ summary: Load the complete Shopify Standard Product Taxonomy into memory as a tree
124
+ test_files: []