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,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductTaxonomy
4
+ module Serializers
5
+ module Attribute
6
+ module Data
7
+ module DataSerializer
8
+ class << self
9
+ def serialize_all
10
+ # Base attributes are sorted by ID, extended attributes are sorted by the order they were added
11
+ extended_attributes, base_attributes = ProductTaxonomy::Attribute.all.partition(&:extended?)
12
+ base_attributes.sort_by!(&:id)
13
+
14
+ {
15
+ "base_attributes" => base_attributes.map { serialize(_1) },
16
+ "extended_attributes" => extended_attributes.map { serialize(_1) },
17
+ }
18
+ end
19
+
20
+ # @param [Attribute] attribute
21
+ # @return [Hash]
22
+ def serialize(attribute)
23
+ if attribute.extended?
24
+ {
25
+ "name" => attribute.name,
26
+ "handle" => attribute.handle,
27
+ "description" => attribute.description,
28
+ "friendly_id" => attribute.friendly_id,
29
+ "values_from" => attribute.values_from.friendly_id,
30
+ }
31
+ else
32
+ {
33
+ "id" => attribute.id,
34
+ "name" => attribute.name,
35
+ "description" => attribute.description,
36
+ "friendly_id" => attribute.friendly_id,
37
+ "handle" => attribute.handle,
38
+ "sorting" => attribute.manually_sorted? ? "custom" : nil,
39
+ "values" => attribute.sorted_values.map(&:friendly_id),
40
+ }.compact
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductTaxonomy
4
+ module Serializers
5
+ module Attribute
6
+ module Data
7
+ module LocalizationsSerializer
8
+ class << self
9
+ def serialize_all
10
+ attributes = ProductTaxonomy::Attribute.all
11
+
12
+ {
13
+ "en" => {
14
+ "attributes" => attributes.sort_by(&:friendly_id).reduce({}) { _1.merge!(serialize(_2)) },
15
+ },
16
+ }
17
+ end
18
+
19
+ # @param [Attribute] attribute
20
+ # @return [Hash]
21
+ def serialize(attribute)
22
+ {
23
+ attribute.friendly_id => {
24
+ "name" => attribute.name,
25
+ "description" => attribute.description,
26
+ },
27
+ }
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductTaxonomy
4
+ module Serializers
5
+ module Attribute
6
+ module Dist
7
+ class JsonSerializer
8
+ class << self
9
+ def serialize_all(version:, locale: "en")
10
+ {
11
+ "version" => version,
12
+ "attributes" => ProductTaxonomy::Attribute.sorted_base_attributes.map { serialize(_1, locale:) },
13
+ }
14
+ end
15
+
16
+ # @param attribute [Attribute]
17
+ # @param locale [String] The locale to use for localized attributes.
18
+ # @return [Hash]
19
+ def serialize(attribute, locale: "en")
20
+ {
21
+ "id" => attribute.gid,
22
+ "name" => attribute.name(locale:),
23
+ "handle" => attribute.handle,
24
+ "description" => attribute.description(locale:),
25
+ "extended_attributes" => attribute.extended_attributes.sort_by(&:name).map do |ext_attr|
26
+ {
27
+ "name" => ext_attr.name(locale:),
28
+ "handle" => ext_attr.handle,
29
+ }
30
+ end,
31
+ "values" => attribute.sorted_values(locale:).map do |value|
32
+ Serializers::Value::Dist::JsonSerializer.serialize(value, locale:)
33
+ end,
34
+ }
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductTaxonomy
4
+ module Serializers
5
+ module Attribute
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 - Attributes: #{version}
12
+ # Format: {GID} : {Attribute name}
13
+
14
+ HEADER
15
+
16
+ attributes_txt = ProductTaxonomy::Attribute
17
+ .sorted_base_attributes
18
+ .map { serialize(_1, padding:, locale:) }
19
+ .join("\n")
20
+
21
+ header + attributes_txt
22
+ end
23
+
24
+ # @param attribute [Attribute]
25
+ # @param padding [Integer] The padding to use for the GID.
26
+ # @param locale [String] The locale to use for localized attributes.
27
+ # @return [String]
28
+ def serialize(attribute, padding: 0, locale: "en")
29
+ "#{attribute.gid.ljust(padding)} : #{attribute.name(locale:)}"
30
+ end
31
+
32
+ private
33
+
34
+ def longest_gid_length
35
+ ProductTaxonomy::Attribute.all.filter_map { _1.extended? ? nil : _1.gid.length }.max
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductTaxonomy
4
+ module Serializers
5
+ module Attribute
6
+ module Docs
7
+ module BaseAndExtendedSerializer
8
+ class << self
9
+ def serialize_all
10
+ ProductTaxonomy::Attribute.sorted_base_attributes.flat_map do |attribute|
11
+ extended_attributes = attribute.extended_attributes.sort_by(&:name).map do |extended_attribute|
12
+ serialize(extended_attribute)
13
+ end
14
+ extended_attributes + [serialize(attribute)]
15
+ end
16
+ end
17
+
18
+ # @param [Attribute] attribute
19
+ # @return [Hash]
20
+ def serialize(attribute)
21
+ result = {
22
+ "id" => attribute.gid,
23
+ "name" => attribute.extended? ? attribute.base_attribute.name : attribute.name,
24
+ "handle" => attribute.handle,
25
+ "extended_name" => attribute.extended? ? attribute.name : nil,
26
+ "values" => attribute.values.map do |value|
27
+ {
28
+ "id" => value.gid,
29
+ "name" => value.name,
30
+ }
31
+ end,
32
+ }
33
+ result.delete("extended_name") unless attribute.extended?
34
+ result
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductTaxonomy
4
+ module Serializers
5
+ module Attribute
6
+ module Docs
7
+ module ReversedSerializer
8
+ class << self
9
+ def serialize_all
10
+ attributes_to_categories = ProductTaxonomy::Category.all.each_with_object({}) do |category, hash|
11
+ category.attributes.each do |attribute|
12
+ hash[attribute] ||= []
13
+ hash[attribute] << category
14
+ end
15
+ end
16
+
17
+ serialized_attributes = ProductTaxonomy::Attribute.all.sort_by(&:name).map do |attribute|
18
+ serialize(attribute, attributes_to_categories[attribute])
19
+ end
20
+
21
+ {
22
+ "attributes" => serialized_attributes,
23
+ }
24
+ end
25
+
26
+ # @param [Attribute] attribute The attribute to serialize.
27
+ # @param [Array<Category>] attribute_categories The categories that the attribute belongs to.
28
+ # @return [Hash] The serialized attribute.
29
+ def serialize(attribute, attribute_categories)
30
+ attribute_categories ||= []
31
+ {
32
+ "id" => attribute.gid,
33
+ "handle" => attribute.handle,
34
+ "name" => attribute.name,
35
+ "base_name" => attribute.extended? ? attribute.base_attribute.name : nil,
36
+ "categories" => attribute_categories.sort_by(&:full_name).map do |category|
37
+ {
38
+ "id" => category.gid,
39
+ "full_name" => category.full_name,
40
+ }
41
+ end,
42
+ "values" => attribute.sorted_values.map do |value|
43
+ {
44
+ "id" => value.gid,
45
+ "name" => value.name,
46
+ }
47
+ end,
48
+ }
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductTaxonomy
4
+ module Serializers
5
+ module Attribute
6
+ module Docs
7
+ module SearchSerializer
8
+ class << self
9
+ def serialize_all
10
+ ProductTaxonomy::Attribute.all.sort_by(&:name).map do |attribute|
11
+ serialize(attribute)
12
+ end
13
+ end
14
+
15
+ # @param [Attribute] attribute
16
+ # @return [Hash]
17
+ def serialize(attribute)
18
+ {
19
+ "searchIdentifier" => attribute.handle,
20
+ "title" => attribute.name,
21
+ "url" => "?attributeHandle=#{attribute.handle}",
22
+ "attribute" => {
23
+ "handle" => attribute.handle,
24
+ },
25
+ }
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductTaxonomy
4
+ module Serializers
5
+ module Category
6
+ module Data
7
+ module DataSerializer
8
+ class << self
9
+ def serialize_all(root = nil)
10
+ categories = root ? root.descendants_and_self : ProductTaxonomy::Category.all_depth_first
11
+ categories.sort_by(&:id_parts).flat_map { serialize(_1) }
12
+ end
13
+
14
+ # @param [Category] category
15
+ # @return [Hash]
16
+ def serialize(category)
17
+ {
18
+ "id" => category.id,
19
+ "name" => category.name,
20
+ "children" => category.children.sort_by(&:id_parts).map(&:id),
21
+ "attributes" => AlphanumericSorter.sort(category.attributes.map(&:friendly_id), other_last: true),
22
+ }
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductTaxonomy
4
+ module Serializers
5
+ module Category
6
+ module Data
7
+ module FullNamesSerializer
8
+ class << self
9
+ def serialize_all
10
+ ProductTaxonomy::Category.all.sort_by(&:full_name).map { serialize(_1) }
11
+ end
12
+
13
+ # @param [Category] category
14
+ # @return [Hash]
15
+ def serialize(category)
16
+ {
17
+ "id" => category.id,
18
+ "full_name" => category.full_name,
19
+ }
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductTaxonomy
4
+ module Serializers
5
+ module Category
6
+ module Data
7
+ module LocalizationsSerializer
8
+ class << self
9
+ def serialize_all
10
+ categories = ProductTaxonomy::Category.all_depth_first
11
+
12
+ {
13
+ "en" => {
14
+ "categories" => categories.sort_by(&:id_parts).reduce({}) { _1.merge!(serialize(_2)) },
15
+ },
16
+ }
17
+ end
18
+
19
+ # @param [Category] category
20
+ # @return [Hash]
21
+ def serialize(category)
22
+ {
23
+ category.id => {
24
+ "name" => category.name,
25
+ "context" => category.full_name,
26
+ },
27
+ }
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductTaxonomy
4
+ module Serializers
5
+ module Category
6
+ module Dist
7
+ module JsonSerializer
8
+ class << self
9
+ def serialize_all(version:, locale: "en")
10
+ {
11
+ "version" => version,
12
+ "verticals" => ProductTaxonomy::Category.verticals.map do |vertical|
13
+ {
14
+ "name" => vertical.name(locale:),
15
+ "prefix" => vertical.id,
16
+ "categories" => vertical.descendants_and_self.map { |category| serialize(category, locale:) },
17
+ }
18
+ end,
19
+ }
20
+ end
21
+
22
+ # @param category [Category]
23
+ # @param locale [String] The locale to use for localization.
24
+ # @return [Hash]
25
+ def serialize(category, locale: "en")
26
+ {
27
+ "id" => category.gid,
28
+ "level" => category.level,
29
+ "name" => category.name(locale:),
30
+ "full_name" => category.full_name(locale:),
31
+ "parent_id" => category.parent&.gid,
32
+ "attributes" => category.attributes.map do |attr|
33
+ {
34
+ "id" => attr.gid,
35
+ "name" => attr.name(locale:),
36
+ "handle" => attr.handle,
37
+ "description" => attr.description(locale:),
38
+ "extended" => attr.is_a?(ExtendedAttribute),
39
+ }
40
+ end,
41
+ "children" => category.children.map do |child|
42
+ {
43
+ "id" => child.gid,
44
+ "name" => child.name(locale:),
45
+ }
46
+ end,
47
+ "ancestors" => category.ancestors.map do |ancestor|
48
+ {
49
+ "id" => ancestor.gid,
50
+ "name" => ancestor.name(locale:),
51
+ }
52
+ end,
53
+ }
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductTaxonomy
4
+ module Serializers
5
+ module Category
6
+ module Dist
7
+ module TxtSerializer
8
+ class << self
9
+ def serialize_all(version:, locale: "en", padding: longest_gid_length)
10
+ header = <<~HEADER
11
+ # Shopify Product Taxonomy - Categories: #{version}
12
+ # Format: {GID} : {Ancestor name} > ... > {Category name}
13
+
14
+ HEADER
15
+
16
+ categories_txt = ProductTaxonomy::Category
17
+ .all_depth_first
18
+ .map { |category| serialize(category, padding:, locale:) }
19
+ .join("\n")
20
+
21
+ header + categories_txt
22
+ end
23
+
24
+ # @param category [Category]
25
+ # @param padding [Integer] The padding to use for the GID.
26
+ # @param locale [String] The locale to use for localization.
27
+ # @return [String]
28
+ def serialize(category, padding:, locale: "en")
29
+ "#{category.gid.ljust(padding)} : #{category.full_name(locale:)}"
30
+ end
31
+
32
+ private
33
+
34
+ def longest_gid_length
35
+ ProductTaxonomy::Category.all.max_by { |c| c.gid.length }.gid.length
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductTaxonomy
4
+ module Serializers
5
+ module Category
6
+ module Docs
7
+ module SearchSerializer
8
+ class << self
9
+ def serialize_all
10
+ ProductTaxonomy::Category.all_depth_first.flat_map { serialize(_1) }
11
+ end
12
+
13
+ # @param [Category] category
14
+ # @return [Hash]
15
+ def serialize(category)
16
+ {
17
+ "searchIdentifier" => category.gid,
18
+ "title" => category.full_name,
19
+ "url" => "?categoryId=#{CGI.escapeURIComponent(category.gid)}",
20
+ "category" => {
21
+ "id" => category.gid,
22
+ "name" => category.name,
23
+ "fully_qualified_type" => category.full_name,
24
+ "depth" => category.level,
25
+ },
26
+ }
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductTaxonomy
4
+ module Serializers
5
+ module Category
6
+ module Docs
7
+ module SiblingsSerializer
8
+ class << self
9
+ def serialize_all
10
+ ProductTaxonomy::Category.all_depth_first.each_with_object({}) do |category, groups|
11
+ parent_id = category.parent&.gid.presence || "root"
12
+ sibling = serialize(category)
13
+
14
+ groups[category.level] ||= {}
15
+ groups[category.level][parent_id] ||= []
16
+ groups[category.level][parent_id] << sibling
17
+ end
18
+ end
19
+
20
+ # @param [Category] category
21
+ # @return [Hash]
22
+ def serialize(category)
23
+ {
24
+ "id" => category.gid,
25
+ "name" => category.name,
26
+ "fully_qualified_type" => category.full_name,
27
+ "depth" => category.level,
28
+ "parent_id" => category.parent&.gid.presence || "root",
29
+ "node_type" => category.root? ? "root" : "leaf",
30
+ "ancestor_ids" => category.ancestors.map(&:gid).join(","),
31
+ "attribute_handles" => category.attributes.map(&:handle).join(","),
32
+ }
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductTaxonomy
4
+ module Serializers
5
+ module Value
6
+ module Data
7
+ module DataSerializer
8
+ class << self
9
+ def serialize_all
10
+ ProductTaxonomy::Value.all.sort_by(&:id).map { serialize(_1) }
11
+ end
12
+
13
+ # @param [Value] value
14
+ # @return [Hash]
15
+ def serialize(value)
16
+ {
17
+ "id" => value.id,
18
+ "name" => value.name,
19
+ "friendly_id" => value.friendly_id,
20
+ "handle" => value.handle,
21
+ }
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductTaxonomy
4
+ module Serializers
5
+ module Value
6
+ module Data
7
+ module LocalizationsSerializer
8
+ class << self
9
+ def serialize_all
10
+ values = ProductTaxonomy::Value.all
11
+
12
+ {
13
+ "en" => {
14
+ "values" => values.sort_by(&:friendly_id).reduce({}) { _1.merge!(serialize(_2)) },
15
+ },
16
+ }
17
+ end
18
+
19
+ # @param [Value] value
20
+ # @return [Hash]
21
+ def serialize(value)
22
+ {
23
+ value.friendly_id => {
24
+ "name" => value.name,
25
+ "context" => value.primary_attribute.name,
26
+ },
27
+ }
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProductTaxonomy
4
+ module Serializers
5
+ module Value
6
+ module Dist
7
+ class JsonSerializer
8
+ class << self
9
+ def serialize_all(version:, locale: "en")
10
+ {
11
+ "version" => version,
12
+ "values" => ProductTaxonomy::Value.all_values_sorted.map { serialize(_1, locale:) },
13
+ }
14
+ end
15
+
16
+ # @param value [Value]
17
+ # @param locale [String] The locale to use for localization.
18
+ # @return [Hash]
19
+ def serialize(value, locale: "en")
20
+ {
21
+ "id" => value.gid,
22
+ "name" => value.name(locale:),
23
+ "handle" => value.handle,
24
+ }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end