Class: ProductTaxonomy::Category

Inherits:
Object
  • Object
show all
Extended by:
Indexed, Localized
Includes:
ActiveModel::Validations, FormattedValidationErrors
Defined in:
lib/product_taxonomy/models/category.rb

Constant Summary

Constants included from Indexed

Indexed::NotFoundError

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Localized

localizations, validate_localizations!

Methods included from Indexed

add, all, create_validate_and_add!, duplicate?, extended, find_by, find_by!, hashed_by, hashed_models, size

Methods included from FormattedValidationErrors

#validate!

Constructor Details

#initialize(id:, name:, attributes: [], return_reasons: [], parent: nil) ⇒ Category

Returns a new instance of Category.

Parameters:

  • id (String)

    The ID of the category.

  • name (String)

    The name of the category.

  • attributes (Array<Attribute>) (defaults to: [])

    The attributes of the category.

  • return_reasons (Array<ReturnReason>, :inherit) (defaults to: [])

    The return reasons for the category, or :inherit to copy them from the closest ancestor that defines its own.

  • parent (Category) (defaults to: nil)

    The parent category of the category.



117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/product_taxonomy/models/category.rb', line 117

def initialize(id:, name:, attributes: [], return_reasons: [], parent: nil)
  @id = id
  @name = name
  @children = []
  @secondary_children = []
  @attributes = attributes
  @inherits_return_reasons = return_reasons == :inherit
  @defined_return_reasons = @inherits_return_reasons ? [] : return_reasons.dup
  @return_reasons = @defined_return_reasons.dup
  @parent = parent
  @secondary_parents = []
end

Class Attribute Details

.verticalsObject (readonly)

Returns the value of attribute verticals.



11
12
13
# File 'lib/product_taxonomy/models/category.rb', line 11

def verticals
  @verticals
end

Instance Attribute Details

#attributesObject (readonly)

Returns the value of attribute attributes.



102
103
104
# File 'lib/product_taxonomy/models/category.rb', line 102

def attributes
  @attributes
end

#childrenObject (readonly)

Returns the value of attribute children.



102
103
104
# File 'lib/product_taxonomy/models/category.rb', line 102

def children
  @children
end

#defined_return_reasonsObject (readonly)

Returns the value of attribute defined_return_reasons.



102
103
104
# File 'lib/product_taxonomy/models/category.rb', line 102

def defined_return_reasons
  @defined_return_reasons
end

#idObject (readonly)

Returns the value of attribute id.



102
103
104
# File 'lib/product_taxonomy/models/category.rb', line 102

def id
  @id
end

#inherits_return_reasonsObject (readonly)

Returns the value of attribute inherits_return_reasons.



102
103
104
# File 'lib/product_taxonomy/models/category.rb', line 102

def inherits_return_reasons
  @inherits_return_reasons
end

#parentObject

Returns the value of attribute parent.



109
110
111
# File 'lib/product_taxonomy/models/category.rb', line 109

def parent
  @parent
end

#return_reasonsObject (readonly)

Returns the value of attribute return_reasons.



102
103
104
# File 'lib/product_taxonomy/models/category.rb', line 102

def return_reasons
  @return_reasons
end

#secondary_childrenObject (readonly)

Returns the value of attribute secondary_children.



102
103
104
# File 'lib/product_taxonomy/models/category.rb', line 102

def secondary_children
  @secondary_children
end

#secondary_parentsObject

Returns the value of attribute secondary_parents.



109
110
111
# File 'lib/product_taxonomy/models/category.rb', line 109

def secondary_parents
  @secondary_parents
end

Class Method Details

.all_depth_firstArray<Category>

Get all categories in depth-first order.

Returns:

  • (Array<Category>)

    The categories in depth-first order.



61
62
63
# File 'lib/product_taxonomy/models/category.rb', line 61

def all_depth_first
  verticals.flat_map(&:descendants_and_self)
end

.load_from_source(source_data) ⇒ Object

Load categories from source data.

Parameters:

  • source_data (Array<Hash>)

    The source data to load categories from.

Raises:

  • (ArgumentError)


16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/product_taxonomy/models/category.rb', line 16

def load_from_source(source_data)
  raise ArgumentError, "source_data must be an array" unless source_data.is_a?(Array)

  # First pass: Create all nodes and add to index
  source_data.each do |item|
    Category.create_validate_and_add!(
      id: item["id"],
      name: item["name"],
      attributes: Array(item["attributes"]).map { Attribute.find_by(friendly_id: _1) || _1 },
      return_reasons: parse_return_reasons(item["return_reasons"]),
    )
  end

  # Second pass: Build relationships
  source_data.each do |item|
    parent = Category.find_by(id: item["id"])
    add_children(type: "children", item:, parent:)
    add_children(type: "secondary_children", item:, parent:)
  end

  # Third pass: Validate all nodes, sort contents, and collect root nodes for verticals
  @verticals = Category.all.each_with_object([]) do |node, root_nodes|
    node.validate!(:category_tree_loaded)
    node.children.sort_by!(&:name)
    node.attributes.sort_by!(&:name)
    # `return_reasons` order is intentionally preserved from `data/categories/*.yml`.
    root_nodes << node if node.root?
  end
  @verticals.sort_by!(&:name)

  # Fourth pass: derive each category's effective return reasons — inherited from the closest defining ancestor,
  # falling back to the global reasons when nothing is defined.
  Category.all.each(&:resolve_return_reasons)
end

.resetObject

Reset all class-level state



52
53
54
55
56
# File 'lib/product_taxonomy/models/category.rb', line 52

def reset
  @localizations = nil
  @hashed_models = nil
  @verticals = nil
end

Instance Method Details

#add_attribute(attribute) ⇒ Object

Add an attribute to the category

Parameters:



159
160
161
# File 'lib/product_taxonomy/models/category.rb', line 159

def add_attribute(attribute)
  @attributes << attribute
end

#add_child(child) ⇒ Object

Add a child to the category

Parameters:

  • child (Category|String)

    node, or the friendly ID if the node was not found.



137
138
139
140
141
142
143
# File 'lib/product_taxonomy/models/category.rb', line 137

def add_child(child)
  @children << child

  return unless child.is_a?(Category)

  child.parent = self
end

#add_return_reason(return_reason) ⇒ Object

Add a return reason to the category. Explicitly adding a reason means the category defines its own reasons rather than inheriting them, so the first add on an inheriting category drops the inherited reasons.

Parameters:



167
168
169
170
171
172
173
174
# File 'lib/product_taxonomy/models/category.rb', line 167

def add_return_reason(return_reason)
  if @inherits_return_reasons
    @inherits_return_reasons = false
    @defined_return_reasons = []
  end
  @defined_return_reasons << return_reason
  resolve_return_reasons
end

#add_secondary_child(child) ⇒ Object

Add a secondary child to the category

Parameters:

  • child (Category|String)

    node, or the friendly ID if the node was not found.



148
149
150
151
152
153
154
# File 'lib/product_taxonomy/models/category.rb', line 148

def add_secondary_child(child)
  @secondary_children << child

  return unless child.is_a?(Category)

  child.secondary_parents << self
end

#ancestorsArray<Category>

The ancestors of the category

Returns:



226
227
228
229
230
# File 'lib/product_taxonomy/models/category.rb', line 226

def ancestors
  return [] if root?

  [parent] + parent.ancestors
end

#descendant_of?(category) ⇒ Boolean

Whether the category is a descendant of another category

Parameters:

Returns:

  • (Boolean)


260
261
262
# File 'lib/product_taxonomy/models/category.rb', line 260

def descendant_of?(category)
  ancestors.include?(category)
end

#descendantsObject

The descendants of the category



273
274
275
# File 'lib/product_taxonomy/models/category.rb', line 273

def descendants
  children.flat_map { |child| [child] + child.descendants }
end

#descendants_and_selfArray<Category>

The descendants of the category and the category itself

Returns:



280
281
282
# File 'lib/product_taxonomy/models/category.rb', line 280

def descendants_and_self
  [self] + descendants
end

#friendly_nameString

The friendly name of the category

Returns:

  • (String)


287
288
289
# File 'lib/product_taxonomy/models/category.rb', line 287

def friendly_name
  "#{id}_#{IdentifierFormatter.format_friendly_id(name)}"
end

#full_name(locale: "en") ⇒ String

The full name of the category

Returns:

  • (String)


235
236
237
238
239
# File 'lib/product_taxonomy/models/category.rb', line 235

def full_name(locale: "en")
  return name(locale:) if root?

  parent.full_name(locale:) + " > " + name(locale:)
end

#gidString

The global ID of the category

Returns:

  • (String)


244
245
246
# File 'lib/product_taxonomy/models/category.rb', line 244

def gid
  "gid://shopify/TaxonomyCategory/#{id}"
end

#id_partsArray<String, Integer>

Split an ID into its parts.

Returns:

  • (Array<String, Integer>)

    The parts of the ID.



251
252
253
254
# File 'lib/product_taxonomy/models/category.rb', line 251

def id_parts
  parts = id.split("-")
  [parts.first] + parts[1..].map(&:to_i)
end

#inspectObject

Information



191
192
193
# File 'lib/product_taxonomy/models/category.rb', line 191

def inspect
  "#<#{self.class.name} id=#{id} name=#{name}>"
end

#leaf?Boolean

Whether the category is a leaf category

Returns:

  • (Boolean)


205
206
207
# File 'lib/product_taxonomy/models/category.rb', line 205

def leaf?
  children.empty?
end

#levelInteger

The level of the category

Returns:

  • (Integer)


212
213
214
# File 'lib/product_taxonomy/models/category.rb', line 212

def level
  ancestors.size
end

#next_child_idString

The next child ID for the category

Returns:

  • (String)


294
295
296
297
298
# File 'lib/product_taxonomy/models/category.rb', line 294

def next_child_id
  largest_child_id = children.map { _1.id.split("-").last.to_i }.max || 0

  "#{id}-#{largest_child_id + 1}"
end

#resolve_return_reasonsObject

Copy return reasons from the closest ancestor that defines its own, when this category inherits. No-op for categories that define their own reasons or have no defining ancestor.



178
179
180
181
182
183
184
185
186
# File 'lib/product_taxonomy/models/category.rb', line 178

def resolve_return_reasons
  @return_reasons = if inherits_return_reasons
    ancestors.find { |ancestor| !ancestor.inherits_return_reasons }&.defined_return_reasons&.dup || []
  else
    defined_return_reasons.dup
  end

  @return_reasons = ReturnReason.global.dup if @return_reasons.empty?
end

#rootCategory

The root category in this category's tree

Returns:



219
220
221
# File 'lib/product_taxonomy/models/category.rb', line 219

def root
  ancestors.last || self
end

#root?Boolean

Whether the category is the root category

Returns:

  • (Boolean)


198
199
200
# File 'lib/product_taxonomy/models/category.rb', line 198

def root?
  parent.nil?
end

#traverse {|Category| ... } ⇒ Object

Iterate over the category and all its descendants

Yields:



267
268
269
270
# File 'lib/product_taxonomy/models/category.rb', line 267

def traverse(&block)
  yield self
  children.each { _1.traverse(&block) }
end