Class: ProductTaxonomy::IntegrationVersion

Inherits:
Object
  • Object
show all
Defined in:
lib/product_taxonomy/models/integration_version.rb

Overview

A single version of an integration, e.g. "shopify/2024-07".

Includes:

  • The full names of categories in the integration's taxonomy.
  • The mapping rules for converting between the integration's taxonomy and Shopify's taxonomy.

Constant Summary collapse

INTEGRATIONS_PATH =
File.expand_path("integrations", ProductTaxonomy.data_path)

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name:, version:, full_names_by_id:, current_shopify_version: nil, from_shopify_mappings: nil, to_shopify_mappings: nil) ⇒ IntegrationVersion

Returns a new instance of IntegrationVersion.

Parameters:

  • name (String)

    The name of the integration.

  • version (String)

    The version of the integration.

  • full_names_by_id (Hash<String, Hash>)

    A hash of full names by ID.

  • current_shopify_version (String) (defaults to: nil)

    The current version of the Shopify taxonomy.

  • from_shopify_mappings (Array<MappingRule>) (defaults to: nil)

    The mappings from the Shopify taxonomy to the integration's taxonomy.

  • to_shopify_mappings (Array<MappingRule>) (defaults to: nil)

    The mappings from the integration's taxonomy to the Shopify taxonomy.



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/product_taxonomy/models/integration_version.rb', line 147

def initialize(
  name:,
  version:,
  full_names_by_id:,
  current_shopify_version: nil,
  from_shopify_mappings: nil,
  to_shopify_mappings: nil
)
  @name = name
  @version = version
  @full_names_by_id = full_names_by_id
  @current_shopify_version = current_shopify_version
  @from_shopify_mappings = from_shopify_mappings
  @to_shopify_mappings = to_shopify_mappings
  @to_json = {} # memoized by direction
end

Instance Attribute Details

#from_shopify_mappingsObject (readonly)

Returns the value of attribute from_shopify_mappings.



137
138
139
# File 'lib/product_taxonomy/models/integration_version.rb', line 137

def from_shopify_mappings
  @from_shopify_mappings
end

#full_names_by_idObject (readonly)

Returns the value of attribute full_names_by_id.



137
138
139
# File 'lib/product_taxonomy/models/integration_version.rb', line 137

def full_names_by_id
  @full_names_by_id
end

#nameObject (readonly)

Returns the value of attribute name.



137
138
139
# File 'lib/product_taxonomy/models/integration_version.rb', line 137

def name
  @name
end

#to_shopify_mappingsObject (readonly)

Returns the value of attribute to_shopify_mappings.



137
138
139
# File 'lib/product_taxonomy/models/integration_version.rb', line 137

def to_shopify_mappings
  @to_shopify_mappings
end

#versionObject (readonly)

Returns the value of attribute version.



137
138
139
# File 'lib/product_taxonomy/models/integration_version.rb', line 137

def version
  @version
end

Class Method Details

.clear_shopify_integrations_directory(output_path:, logger:) ⇒ Object

Clear the Shopify integrations directory before generating new files.

Parameters:

  • output_path (String)

    The path to the output directory.

  • logger (Logger)

    The logger to use for logging messages.



35
36
37
38
39
40
41
# File 'lib/product_taxonomy/models/integration_version.rb', line 35

def clear_shopify_integrations_directory(output_path:, logger:)
  shopify_integrations_path = File.join(integrations_output_path(output_path), "shopify")
  if Dir.exist?(shopify_integrations_path)
    FileUtils.rm_rf(shopify_integrations_path)
    FileUtils.mkdir_p(shopify_integrations_path)
  end
end

.generate_all_distributions(output_path:, logger:, current_shopify_version: nil, base_path: INTEGRATIONS_PATH) ⇒ Object

Generate all distribution files for all integration versions.

Parameters:

  • output_path (String)

    The path to the output directory.

  • logger (Logger)

    The logger to use for logging messages.

  • current_shopify_version (String) (defaults to: nil)

    The current version of the Shopify taxonomy.

  • base_path (String) (defaults to: INTEGRATIONS_PATH)

    The path to the base directory containing integration versions.



19
20
21
22
23
24
25
26
27
28
29
# File 'lib/product_taxonomy/models/integration_version.rb', line 19

def generate_all_distributions(output_path:, logger:, current_shopify_version: nil, base_path: INTEGRATIONS_PATH)
  clear_shopify_integrations_directory(output_path:, logger:)
  
  integration_versions = load_all_from_source(current_shopify_version:, base_path:)
  all_mappings = integration_versions.each_with_object([]) do |integration_version, all_mappings|
    logger.info("Generating integration mappings for #{integration_version.name}/#{integration_version.version}")
    integration_version.generate_distributions(output_path:)
    all_mappings.concat(integration_version.to_json(direction: :both))
  end
  generate_all_mappings_file(mappings: all_mappings, current_shopify_version:, output_path:)
end

.generate_all_mappings_file(mappings:, current_shopify_version:, output_path:) ⇒ Object

Generate a JSON file containing all mappings for all integration versions.

Parameters:

  • mappings (Array<Hash>)

    The mappings to include in the file.

  • version (String)

    The current version of the Shopify taxonomy.

  • output_path (String)

    The path to the output directory.



109
110
111
112
113
114
# File 'lib/product_taxonomy/models/integration_version.rb', line 109

def generate_all_mappings_file(mappings:, current_shopify_version:, output_path:)
  File.write(
    File.expand_path("all_mappings.json", integrations_output_path(output_path)),
    JSON.pretty_generate(to_json(mappings:, current_shopify_version:)) + "\n",
  )
end

.integrations_output_path(base_output_path) ⇒ String

Generate the path to the integrations output directory.

Parameters:

  • base_output_path (String)

    The base path to the output directory.

Returns:

  • (String)


132
133
134
# File 'lib/product_taxonomy/models/integration_version.rb', line 132

def integrations_output_path(base_output_path)
  File.expand_path("en/integrations", base_output_path)
end

.load_all_from_source(current_shopify_version: nil, base_path: INTEGRATIONS_PATH) ⇒ Array<IntegrationVersion>

Load all integration versions from the source data directory.

Returns:



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/product_taxonomy/models/integration_version.rb', line 46

def load_all_from_source(current_shopify_version: nil, base_path: INTEGRATIONS_PATH)
  integrations_yaml = YAML.safe_load_file(File.expand_path("integrations.yml", base_path))
  integrations_yaml.flat_map do |integration_yaml|
    versions = integration_yaml["available_versions"].sort.map do |version_path|
      load_from_source(
        integration_path: File.expand_path(version_path, base_path),
        current_shopify_version:,
      )
    end

    resolve_to_shopify_mappings_chain(versions) if integration_yaml["name"] == "shopify"

    versions
  end
end

.load_from_source(integration_path:, current_shopify_version: nil) ⇒ IntegrationVersion

Load an integration version from the provided source data directory.

Parameters:

  • integration_path (String)

    The path to the integration version source data directory.

Returns:



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/product_taxonomy/models/integration_version.rb', line 77

def load_from_source(integration_path:, current_shopify_version: nil)
  full_names = YAML.safe_load_file(File.expand_path("full_names.yml", integration_path))
  full_names_by_id = full_names.each_with_object({}) { |data, hash| hash[data["id"].to_s] = data }

  from_shopify_mappings = MappingRule.load_rules_from_source(
    integration_path:,
    direction: :from_shopify,
    full_names_by_id:,
  )
  to_shopify_mappings = MappingRule.load_rules_from_source(
    integration_path:,
    direction: :to_shopify,
    full_names_by_id:,
  )

  integration_pathname = Pathname.new(integration_path)

  new(
    name: integration_pathname.parent.basename.to_s,
    version: integration_pathname.basename.to_s,
    full_names_by_id:,
    from_shopify_mappings:,
    to_shopify_mappings:,
    current_shopify_version:,
  )
end

.resolve_to_shopify_mappings_chain(versions) ⇒ Object

Resolve a set of IntegrationVersion to_shopify mappings so that each one maps to the latest version of the Shopify taxonomy.

Parameters:

  • versions (Array<IntegrationVersion>)

    The versions to resolve, ordered from oldest to newest.



66
67
68
69
70
71
# File 'lib/product_taxonomy/models/integration_version.rb', line 66

def resolve_to_shopify_mappings_chain(versions)
  versions.reverse.each_with_object([]) do |version, resolved_versions|
    version.resolve_to_shopify_mappings(resolved_versions)
    resolved_versions.prepend(version)
  end
end

.to_json(current_shopify_version:, mappings:) ⇒ Hash

Generate a JSON representation for a given set of mappings and version of the Shopify taxonomy.

Parameters:

  • version (String)

    The current version of the Shopify taxonomy.

  • mappings (Array<Hash>)

    The mappings to include in the file.

Returns:

  • (Hash)


121
122
123
124
125
126
# File 'lib/product_taxonomy/models/integration_version.rb', line 121

def to_json(current_shopify_version:, mappings:)
  {
    version: current_shopify_version,
    mappings:,
  }
end

Instance Method Details

#generate_distribution(output_path:, direction:) ⇒ Object

Generate JSON and TXT distribution files for a single direction of the integration version.

Parameters:

  • output_path (String)

    The path to the output directory.

  • direction (Symbol)

    The direction of the distribution file to generate (:from_shopify or :to_shopify).



176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/product_taxonomy/models/integration_version.rb', line 176

def generate_distribution(output_path:, direction:)
  output_dir = File.expand_path(@name, self.class.integrations_output_path(output_path))
  FileUtils.mkdir_p(output_dir)

  json = self.class.to_json(mappings: [to_json(direction:)], current_shopify_version: @current_shopify_version)
  File.write(
    File.expand_path("#{distribution_filename(direction:)}.json", output_dir),
    JSON.pretty_generate(json) + "\n",
  )
  File.write(
    File.expand_path("#{distribution_filename(direction:)}.txt", output_dir),
    to_txt(direction:) + "\n",
  )
end

#generate_distributions(output_path:) ⇒ Object

Generate all distribution files for the integration version.

Parameters:

  • output_path (String)

    The path to the output directory.



167
168
169
170
# File 'lib/product_taxonomy/models/integration_version.rb', line 167

def generate_distributions(output_path:)
  generate_distribution(output_path:, direction: :from_shopify) if @from_shopify_mappings.present?
  generate_distribution(output_path:, direction: :to_shopify) if @to_shopify_mappings.present?
end

#resolve_to_shopify_mappings(next_integration_versions) ⇒ Object

Resolve the output categories of to_shopify mappings to the current version of the Shopify taxonomy, taking into any mappings from later versions.

Parameters:

  • next_integration_versions (Array<IntegrationVersion>)

    An array of Shopify integration versions coming after the current version.



196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/product_taxonomy/models/integration_version.rb', line 196

def resolve_to_shopify_mappings(next_integration_versions)
  @to_shopify_mappings&.each do |mapping|
    newer_mapping = next_integration_versions.flat_map(&:to_shopify_mappings).compact.find do |mapping_rule|
      mapping_rule.input_category["id"] == mapping.output_category
    end
    mapping.output_category = newer_mapping&.output_category || Category.find_by(id: mapping.output_category)

    next unless mapping.output_category.nil?

    raise ArgumentError, "Failed to resolve Shopify mapping: " \
      "\"#{mapping.input_category["id"]}\" to \"#{mapping.output_category}\" " \
      "(input version: #{version})"
  end
end

#to_json(direction:) ⇒ Hash, ...

Generate a JSON representation of the integration version for a single direction.

Parameters:

  • direction (Symbol)

    The direction of the distribution file to generate (:from_shopify or :to_shopify).

Returns:

  • (Hash, Array<Hash>, nil)


226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/product_taxonomy/models/integration_version.rb', line 226

def to_json(direction:)
  if @to_json.key?(direction)
    @to_json[direction]
  elsif direction == :both
    [to_json(direction: :from_shopify), to_json(direction: :to_shopify)].compact
  else
    mappings = if direction == :from_shopify
      @from_shopify_mappings&.sort_by { _1.input_category.id_parts }
    else
      @to_shopify_mappings
    end

    @to_json[direction] = if mappings.present?
      {
        input_taxonomy: input_name_and_version(direction:),
        output_taxonomy: output_name_and_version(direction:),
        rules: mappings.map(&:to_json),
      }
    end
  end
end

#to_txt(direction:) ⇒ String

Generate a TXT representation of the integration version for a single direction.

Parameters:

  • direction (Symbol)

    The direction of the distribution file to generate (:from_shopify or :to_shopify).

Returns:

  • (String)


252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/product_taxonomy/models/integration_version.rb', line 252

def to_txt(direction:)
  mappings = direction == :from_shopify ? @from_shopify_mappings : @to_shopify_mappings

  header = <<~TXT
    # Shopify Product Taxonomy - Mapping #{input_name_and_version(direction:)} to #{output_name_and_version(direction:)}
    # Format:
    # → {base taxonomy category name}
    # ⇒ {mapped taxonomy category name}

  TXT

  visible_mappings = mappings.filter_map do |mapping|
    next if @name == "shopify" && direction == :to_shopify && mapping.input_txt_equals_output_txt?

    mapping.to_txt
  end

  header + visible_mappings.sort.join("\n").chomp
end

#unmapped_external_category_idsArray<String>

For a mapping to an external taxonomy, get the IDs of external categories that are not mapped from Shopify.

Returns:

  • (Array<String>)

    IDs of external categories not mapped from the Shopify taxonomy. Empty if there are no mappings from Shopify.



215
216
217
218
219
220
# File 'lib/product_taxonomy/models/integration_version.rb', line 215

def unmapped_external_category_ids
  return [] if @from_shopify_mappings.blank?

  mappings_by_output_category_id = @from_shopify_mappings.index_by { _1.output_category["id"].to_s }
  @full_names_by_id.keys - mappings_by_output_category_id.keys
end