Skip to content

Commit

Permalink
WIP: Migrate generate_docs command to new tool
Browse files Browse the repository at this point in the history
  • Loading branch information
danielpgross committed Jan 24, 2025
1 parent d508022 commit d8e7b6c
Show file tree
Hide file tree
Showing 18 changed files with 1,025 additions and 5 deletions.
6 changes: 6 additions & 0 deletions dev/lib/product_taxonomy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ def data_path = DATA_PATH
require_relative "product_taxonomy/models/taxonomy"
require_relative "product_taxonomy/models/mapping_rule"
require_relative "product_taxonomy/models/integration_version"
require_relative "product_taxonomy/models/serializers/category/docs/siblings_serializer"
require_relative "product_taxonomy/models/serializers/category/docs/search_serializer"
require_relative "product_taxonomy/models/serializers/attribute/docs/base_and_extended_serializer"
require_relative "product_taxonomy/models/serializers/attribute/docs/reversed_serializer"
require_relative "product_taxonomy/models/serializers/attribute/docs/search_serializer"
require_relative "product_taxonomy/commands/command"
require_relative "product_taxonomy/commands/generate_dist_command"
require_relative "product_taxonomy/commands/find_unmapped_external_categories_command"
require_relative "product_taxonomy/commands/generate_docs_command"
6 changes: 6 additions & 0 deletions dev/lib/product_taxonomy/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,11 @@ def dist
def unmapped_external_categories(name_and_version)
FindUnmappedExternalCategoriesCommand.new(options).run(name_and_version)
end

desc "docs", "Generate documentation files"
option :version, type: :string, desc: "The version of the documentation to generate"
def docs
GenerateDocsCommand.new(options).run
end
end
end
12 changes: 12 additions & 0 deletions dev/lib/product_taxonomy/commands/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,17 @@ def load_taxonomy
end
ProductTaxonomy::Category.load_from_source(categories_source_data)
end

def validate_and_sanitize_version!(version)
return version if version.nil?

sanitized_version = version.to_s.strip
unless sanitized_version.match?(/\A[a-zA-Z0-9.-]+\z/) && !sanitized_version.include?("..")
raise ArgumentError,
"Invalid version format. Version can only contain alphanumeric characters, dots, and dashes."
end

sanitized_version
end
end
end
103 changes: 103 additions & 0 deletions dev/lib/product_taxonomy/commands/generate_docs_command.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# frozen_string_literal: true

module ProductTaxonomy
class GenerateDocsCommand < Command
UNSTABLE = "unstable"

class << self
def docs_path
File.expand_path("../docs", ProductTaxonomy.data_path)
end
end

def initialize(options)
super

@version = validate_and_sanitize_version!(options[:version]) || UNSTABLE
end

def execute
logger.info("Version: #{@version}")

load_taxonomy
generate_data_files
generate_release_folder unless @version == UNSTABLE
end

private

def generate_data_files
data_target = File.expand_path("_data/#{@version}", self.class.docs_path)
FileUtils.mkdir_p(data_target)

logger.info("Generating sibling groups...")
sibling_groups_yaml = YAML.dump(Serializers::Category::Docs::SiblingsSerializer.serialize_all, line_width: -1)
File.write("#{data_target}/sibling_groups.yml", sibling_groups_yaml)

logger.info("Generating category search index...")
search_index_json = JSON.fast_generate(Serializers::Category::Docs::SearchSerializer.serialize_all)
File.write("#{data_target}/search_index.json", search_index_json + "\n")

logger.info("Generating attributes...")
attributes_yml = YAML.dump(Serializers::Attribute::Docs::BaseAndExtendedSerializer.serialize_all, line_width: -1)
File.write("#{data_target}/attributes.yml", attributes_yml)

logger.info("Generating mappings...")
mappings_json = JSON.parse(File.read(File.expand_path(
"../dist/en/integrations/all_mappings.json",
ProductTaxonomy.data_path,
)))
mappings_data = reverse_shopify_mapping_rules(mappings_json.fetch("mappings"))
mappings_yml = YAML.dump(mappings_data, line_width: -1)
File.write("#{data_target}/mappings.yml", mappings_yml)

logger.info("Generating attributes with categories...")
reversed_attributes_yml = YAML.dump(
Serializers::Attribute::Docs::ReversedSerializer.serialize_all,
line_width: -1,
)
File.write("#{data_target}/reversed_attributes.yml", reversed_attributes_yml)

logger.info("Generating attribute with categories search index...")
attribute_search_index_json = JSON.fast_generate(Serializers::Attribute::Docs::SearchSerializer.serialize_all)
File.write("#{data_target}/attribute_search_index.json", attribute_search_index_json + "\n")
end

def generate_release_folder
logger.info("Generating release folder...")

release_path = File.expand_path("_releases/#{@version}", self.class.docs_path)
FileUtils.mkdir_p(release_path)

logger.info("Generating index.html...")
content = File.read(File.expand_path("_releases/_index_template.html", self.class.docs_path))
content.gsub!("TITLE", @version.upcase)
content.gsub!("TARGET", @version)
content.gsub!("GH_URL", "https://github.com/Shopify/product-taxonomy/releases/tag/v#{@version}")
File.write("#{release_path}/index.html", content)

logger.info("Generating attributes.html...")
content = File.read(File.expand_path("_releases/_attributes_template.html", self.class.docs_path))
content.gsub!("TITLE", @version.upcase)
content.gsub!("TARGET", @version)
content.gsub!("GH_URL", "https://github.com/Shopify/product-taxonomy/releases/tag/v#{@version}")
File.write("#{release_path}/attributes.html", content)
end

def reverse_shopify_mapping_rules(mappings)
mappings.each do |mapping|
next unless mapping["output_taxonomy"].include?("shopify")

mapping["input_taxonomy"], mapping["output_taxonomy"] = mapping["output_taxonomy"], mapping["input_taxonomy"]
mapping["rules"] = mapping["rules"].flat_map do |rule|
rule["output"]["category"].map do |output_category|
{
"input" => { "category" => output_category },
"output" => { "category" => [rule["input"]["category"]] },
}
end
end
end
end
end
end
11 changes: 7 additions & 4 deletions dev/lib/product_taxonomy/models/attribute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ def reset
@hashed_models = nil
end

# Get base attributes only, sorted by name.
#
# @return [Array<Attribute>] The sorted base attributes.
def sorted_base_attributes
all.reject(&:extended?).sort_by(&:name)
end

private

def attribute_from(attribute_data)
Expand Down Expand Up @@ -93,10 +100,6 @@ def extended_attribute_from(attribute_data)
def longest_gid_length
all.filter_map { _1.extended? ? nil : _1.gid.length }.max
end

def sorted_base_attributes
all.reject(&:extended?).sort_by(&:name)
end
end

validates :id, presence: true, numericality: { only_integer: true }, if: -> { self.class == Attribute }
Expand Down
9 changes: 8 additions & 1 deletion dev/lib/product_taxonomy/models/category.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def to_txt(version:, locale: "en", padding: longest_gid_length)
HEADER
[
header,
*verticals.flat_map(&:descendants_and_self).map { _1.to_txt(padding:, locale:) },
*all_depth_first.map { _1.to_txt(padding:, locale:) },
].join("\n")
end

Expand All @@ -84,6 +84,13 @@ def reset
@verticals = nil
end

# Get all categories in depth-first order.
#
# @return [Array<Category>] The categories in depth-first order.
def all_depth_first
verticals.flat_map(&:descendants_and_self)
end

private

def add_children(type:, item:, parent:)
Expand Down
2 changes: 2 additions & 0 deletions dev/lib/product_taxonomy/models/extended_attribute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ def localizations

attr_reader :values_from

alias_method :base_attribute, :values_from

# @param name [String] The name of the attribute.
# @param handle [String] The handle of the attribute.
# @param description [String] The description of the attribute.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

module ProductTaxonomy
module Serializers
module Attribute
module Docs
module BaseAndExtendedSerializer
class << self
def serialize_all
ProductTaxonomy::Attribute.sorted_base_attributes.flat_map do |attribute|
extended_attributes = attribute.extended_attributes.sort_by(&:name).map do |extended_attribute|
serialize(extended_attribute)
end
extended_attributes + [serialize(attribute)]
end
end

# @param [Attribute] attribute
# @return [Hash]
def serialize(attribute)
result = {
"id" => attribute.gid,
"name" => attribute.extended? ? attribute.base_attribute.name : attribute.name,
"handle" => attribute.handle,
"extended_name" => attribute.extended? ? attribute.name : nil,
"values" => attribute.values.map do |value|
{
"id" => value.gid,
"name" => value.name,
}
end,
}
result.delete("extended_name") unless attribute.extended?
result
end
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# frozen_string_literal: true

module ProductTaxonomy
module Serializers
module Attribute
module Docs
module ReversedSerializer
class << self
def serialize_all
attributes_to_categories = ProductTaxonomy::Category.all.each_with_object({}) do |category, hash|
category.attributes.each do |attribute|
hash[attribute] ||= []
hash[attribute] << category
end
end

serialized_attributes = ProductTaxonomy::Attribute.all.sort_by(&:name).map do |attribute|
serialize(attribute, attributes_to_categories[attribute])
end

{
"attributes" => serialized_attributes,
}
end

# @param [Attribute] attribute The attribute to serialize.
# @param [Array<Category>] attribute_categories The categories that the attribute belongs to.
# @return [Hash] The serialized attribute.
def serialize(attribute, attribute_categories)
attribute_categories ||= []
{
"id" => attribute.gid,
"handle" => attribute.handle,
"name" => attribute.name,
"base_name" => attribute.extended? ? attribute.base_attribute.name : nil,
"categories" => attribute_categories.sort_by(&:full_name).map do |category|
{
"id" => category.gid,
"full_name" => category.full_name,
}
end,
"values" => attribute.sorted_values.map do |value|
{
"id" => value.gid,
"name" => value.name,
}
end,
}
end
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

module ProductTaxonomy
module Serializers
module Attribute
module Docs
module SearchSerializer
class << self
def serialize_all
ProductTaxonomy::Attribute.all.sort_by(&:name).map do |attribute|
serialize(attribute)
end
end

# @param [Attribute] attribute
# @return [Hash]
def serialize(attribute)
{
"searchIdentifier" => attribute.handle,
"title" => attribute.name,
"url" => "?attributeHandle=#{attribute.handle}",
"attribute" => {
"handle" => attribute.handle,
},
}
end
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

module ProductTaxonomy
module Serializers
module Category
module Docs
module SearchSerializer
class << self
def serialize_all
ProductTaxonomy::Category.all_depth_first.flat_map { serialize(_1) }
end

# @param [Category] category
# @return [Hash]
def serialize(category)
{
"searchIdentifier" => category.gid,
"title" => category.full_name,
"url" => "?categoryId=#{CGI.escapeURIComponent(category.gid)}",
"category" => {
"id" => category.gid,
"name" => category.name,
"fully_qualified_type" => category.full_name,
"depth" => category.level,
},
}
end
end
end
end
end
end
end
Loading

0 comments on commit d8e7b6c

Please sign in to comment.