diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 1c85b5d66..000000000 --- a/.gitattributes +++ /dev/null @@ -1,6 +0,0 @@ -# See https://git-scm.com/docs/gitattributes for more about git attribute files. - -# Mark any vendored files as having been vendored. -vendor/* linguist-vendored -config/credentials/*.yml.enc diff=rails_credentials -config/credentials.yml.enc diff=rails_credentials diff --git a/.gitignore b/.gitignore index 3f41d3523..a1297f8c1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,38 +3,3 @@ /docs/_data/unstable /docs/.jekyll-metadata node_modules - -# Ignore bundler config. -/.bundle - -# Ignore all environment files (except templates). -/.env* -!/.env*.erb - -# Ignore all environment-specific local overrides -.env.development.local -.env.test.local - -# Ignore all logfiles and tempfiles. -/log/* -/tmp/* - -# Ignore pidfiles, but keep the directory. -/tmp/pids/* - -# Ignore storage (uploaded files in development and any SQLite databases). -/storage/* -/tmp/storage/* - -# Ignore master key for decrypting credentials and more. -/config/master.key - -# Ignore build manifests -*.b3 -*.b3.sig - -# Ignore local scripts folder and its contents -/local_scripts - -# Ignore exports folder and its contents -/exports diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 3a30ab5ee..000000000 --- a/.prettierignore +++ /dev/null @@ -1,7 +0,0 @@ -# Ignore all directories -/* - -# Except: -!/docs -/docs/* -!/docs/assets diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index ae92bb754..000000000 --- a/.prettierrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "singleQuote": true, - "bracketSpacing": false, - "trailingComma": "all" -} diff --git a/.rubocop.yml b/.rubocop.yml deleted file mode 100644 index 7353f1aed..000000000 --- a/.rubocop.yml +++ /dev/null @@ -1,6 +0,0 @@ -inherit_gem: - rubocop-shopify: rubocop.yml - -AllCops: - Exclude: - - 'bin/bundle' diff --git a/.ruby-version b/.ruby-version deleted file mode 100644 index 15a279981..000000000 --- a/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -3.3.0 diff --git a/Gemfile b/Gemfile deleted file mode 100644 index c19f1e609..000000000 --- a/Gemfile +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -source "https://rubygems.org" - -# core app -gem "rails", "~> 7.1.3", ">= 7.1.3.2" -gem "sqlite3", "~> 1.7" -gem "puma", ">= 5.0" -gem "tzinfo-data", platforms: [:windows, :jruby] - -gem "bootsnap", require: false -gem "rubocop-shopify", require: false - -# docs -gem "jekyll", "~> 4.3" -gem "jekyll-redirect-from", "~> 0.16" - -# command line -gem "cli-ui", "~> 2.2", require: false -gem "tty-option", "~> 0.3", require: false - -# generate taxonomy mappings -gem "qdrant-ruby", require: "qdrant" -gem "ruby-openai" -gem 'dotenv', groups: [:development, :test] - -group :development, :test do - gem "debug", platforms: [:mri, :windows] - gem "mocha" - gem "factory_bot_rails", "~> 6.4" - gem "minitest-hooks", "~> 1.5" -end diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 65301960e..000000000 --- a/Gemfile.lock +++ /dev/null @@ -1,385 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - actioncable (7.1.4.1) - actionpack (= 7.1.4.1) - activesupport (= 7.1.4.1) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - zeitwerk (~> 2.6) - actionmailbox (7.1.4.1) - actionpack (= 7.1.4.1) - activejob (= 7.1.4.1) - activerecord (= 7.1.4.1) - activestorage (= 7.1.4.1) - activesupport (= 7.1.4.1) - mail (>= 2.7.1) - net-imap - net-pop - net-smtp - actionmailer (7.1.4.1) - actionpack (= 7.1.4.1) - actionview (= 7.1.4.1) - activejob (= 7.1.4.1) - activesupport (= 7.1.4.1) - mail (~> 2.5, >= 2.5.4) - net-imap - net-pop - net-smtp - rails-dom-testing (~> 2.2) - actionpack (7.1.4.1) - actionview (= 7.1.4.1) - activesupport (= 7.1.4.1) - nokogiri (>= 1.8.5) - racc - rack (>= 2.2.4) - rack-session (>= 1.0.1) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.2) - rails-html-sanitizer (~> 1.6) - actiontext (7.1.4.1) - actionpack (= 7.1.4.1) - activerecord (= 7.1.4.1) - activestorage (= 7.1.4.1) - activesupport (= 7.1.4.1) - globalid (>= 0.6.0) - nokogiri (>= 1.8.5) - actionview (7.1.4.1) - activesupport (= 7.1.4.1) - builder (~> 3.1) - erubi (~> 1.11) - rails-dom-testing (~> 2.2) - rails-html-sanitizer (~> 1.6) - activejob (7.1.4.1) - activesupport (= 7.1.4.1) - globalid (>= 0.3.6) - activemodel (7.1.4.1) - activesupport (= 7.1.4.1) - activerecord (7.1.4.1) - activemodel (= 7.1.4.1) - activesupport (= 7.1.4.1) - timeout (>= 0.4.0) - activestorage (7.1.4.1) - actionpack (= 7.1.4.1) - activejob (= 7.1.4.1) - activerecord (= 7.1.4.1) - activesupport (= 7.1.4.1) - marcel (~> 1.0) - activesupport (7.1.4.1) - base64 - bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) - connection_pool (>= 2.2.5) - drb - i18n (>= 1.6, < 2) - minitest (>= 5.1) - mutex_m - tzinfo (~> 2.0) - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) - ast (2.4.2) - base64 (0.2.0) - bigdecimal (3.1.8) - bootsnap (1.18.3) - msgpack (~> 1.2) - builder (3.3.0) - cli-ui (2.2.3) - colorator (1.1.0) - concurrent-ruby (1.3.4) - connection_pool (2.4.1) - crass (1.0.6) - date (3.3.4) - debug (1.9.2) - irb (~> 1.10) - reline (>= 0.3.8) - dotenv (3.1.2) - drb (2.2.1) - em-websocket (0.5.3) - eventmachine (>= 0.12.9) - http_parser.rb (~> 0) - erubi (1.13.0) - event_stream_parser (1.0.0) - eventmachine (1.2.7) - factory_bot (6.4.6) - activesupport (>= 5.0.0) - factory_bot_rails (6.4.3) - factory_bot (~> 6.4) - railties (>= 5.0.0) - faraday (2.10.0) - faraday-net_http (>= 2.0, < 3.2) - logger - faraday-multipart (1.0.4) - multipart-post (~> 2) - faraday-net_http (3.1.1) - net-http - ffi (1.16.3) - forwardable-extended (2.6.0) - globalid (1.2.1) - activesupport (>= 6.1) - google-protobuf (4.27.5) - bigdecimal - rake (>= 13) - google-protobuf (4.27.5-aarch64-linux) - bigdecimal - rake (>= 13) - google-protobuf (4.27.5-arm64-darwin) - bigdecimal - rake (>= 13) - google-protobuf (4.27.5-x86-linux) - bigdecimal - rake (>= 13) - google-protobuf (4.27.5-x86_64-darwin) - bigdecimal - rake (>= 13) - google-protobuf (4.27.5-x86_64-linux) - bigdecimal - rake (>= 13) - http_parser.rb (0.8.0) - i18n (1.14.6) - concurrent-ruby (~> 1.0) - io-console (0.7.2) - irb (1.13.1) - rdoc (>= 4.0.0) - reline (>= 0.4.2) - jekyll (4.3.3) - addressable (~> 2.4) - colorator (~> 1.0) - em-websocket (~> 0.5) - i18n (~> 1.0) - jekyll-sass-converter (>= 2.0, < 4.0) - jekyll-watch (~> 2.0) - kramdown (~> 2.3, >= 2.3.1) - kramdown-parser-gfm (~> 1.0) - liquid (~> 4.0) - mercenary (>= 0.3.6, < 0.5) - pathutil (~> 0.9) - rouge (>= 3.0, < 5.0) - safe_yaml (~> 1.0) - terminal-table (>= 1.8, < 4.0) - webrick (~> 1.7) - jekyll-redirect-from (0.16.0) - jekyll (>= 3.3, < 5.0) - jekyll-sass-converter (3.0.0) - sass-embedded (~> 1.54) - jekyll-watch (2.2.1) - listen (~> 3.0) - json (2.7.2) - kramdown (2.4.0) - rexml - kramdown-parser-gfm (1.1.0) - kramdown (~> 2.0) - language_server-protocol (3.17.0.3) - liquid (4.0.4) - listen (3.9.0) - rb-fsevent (~> 0.10, >= 0.10.3) - rb-inotify (~> 0.9, >= 0.9.10) - logger (1.6.0) - loofah (2.22.0) - crass (~> 1.0.2) - nokogiri (>= 1.12.0) - mail (2.8.1) - mini_mime (>= 0.1.1) - net-imap - net-pop - net-smtp - marcel (1.0.4) - mercenary (0.4.0) - mini_mime (1.1.5) - minitest (5.25.1) - minitest-hooks (1.5.1) - minitest (> 5.3) - mocha (2.3.0) - ruby2_keywords (>= 0.0.5) - msgpack (1.7.2) - multipart-post (2.4.1) - mutex_m (0.2.0) - net-http (0.4.1) - uri - net-imap (0.4.11) - date - net-protocol - net-pop (0.1.2) - net-protocol - net-protocol (0.2.2) - timeout - net-smtp (0.5.0) - net-protocol - nio4r (2.7.3) - nokogiri (1.16.7-aarch64-linux) - racc (~> 1.4) - nokogiri (1.16.7-arm-linux) - racc (~> 1.4) - nokogiri (1.16.7-arm64-darwin) - racc (~> 1.4) - nokogiri (1.16.7-x86-linux) - racc (~> 1.4) - nokogiri (1.16.7-x86_64-darwin) - racc (~> 1.4) - nokogiri (1.16.7-x86_64-linux) - racc (~> 1.4) - parallel (1.24.0) - parser (3.3.1.0) - ast (~> 2.4.1) - racc - pathutil (0.16.2) - forwardable-extended (~> 2.6) - psych (5.1.2) - stringio - public_suffix (5.0.5) - puma (6.4.3) - nio4r (~> 2.0) - qdrant-ruby (0.9.7) - faraday (>= 2.0.1, < 3) - racc (1.8.1) - rack (3.1.8) - rack-session (2.0.0) - rack (>= 3.0.0) - rack-test (2.1.0) - rack (>= 1.3) - rackup (2.1.0) - rack (>= 3) - webrick (~> 1.8) - rails (7.1.4.1) - actioncable (= 7.1.4.1) - actionmailbox (= 7.1.4.1) - actionmailer (= 7.1.4.1) - actionpack (= 7.1.4.1) - actiontext (= 7.1.4.1) - actionview (= 7.1.4.1) - activejob (= 7.1.4.1) - activemodel (= 7.1.4.1) - activerecord (= 7.1.4.1) - activestorage (= 7.1.4.1) - activesupport (= 7.1.4.1) - bundler (>= 1.15.0) - railties (= 7.1.4.1) - rails-dom-testing (2.2.0) - activesupport (>= 5.0.0) - minitest - nokogiri (>= 1.6) - rails-html-sanitizer (1.6.0) - loofah (~> 2.21) - nokogiri (~> 1.14) - railties (7.1.4.1) - actionpack (= 7.1.4.1) - activesupport (= 7.1.4.1) - irb - rackup (>= 1.0.0) - rake (>= 12.2) - thor (~> 1.0, >= 1.2.2) - zeitwerk (~> 2.6) - rainbow (3.1.1) - rake (13.2.1) - rb-fsevent (0.11.2) - rb-inotify (0.11.1) - ffi (~> 1.0) - rdoc (6.6.3.1) - psych (>= 4.0.0) - regexp_parser (2.9.2) - reline (0.5.7) - io-console (~> 0.5) - rexml (3.3.6) - strscan - rouge (4.2.1) - rubocop (1.63.5) - json (~> 2.3) - language_server-protocol (>= 3.17.0) - parallel (~> 1.10) - parser (>= 3.3.0.2) - rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.31.1, < 2.0) - ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.31.3) - parser (>= 3.3.1.0) - rubocop-shopify (2.15.1) - rubocop (~> 1.51) - ruby-openai (7.1.0) - event_stream_parser (>= 0.3.0, < 2.0.0) - faraday (>= 1) - faraday-multipart (>= 1) - ruby-progressbar (1.13.0) - ruby2_keywords (0.0.5) - safe_yaml (1.0.5) - sass-embedded (1.77.2-aarch64-linux-gnu) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.77.2-aarch64-linux-musl) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.77.2-arm-linux-gnueabihf) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.77.2-arm-linux-musleabihf) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.77.2-arm64-darwin) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.77.2-x86-linux-gnu) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.77.2-x86-linux-musl) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.77.2-x86_64-darwin) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.77.2-x86_64-linux-gnu) - google-protobuf (>= 3.25, < 5.0) - sass-embedded (1.77.2-x86_64-linux-musl) - google-protobuf (>= 3.25, < 5.0) - sqlite3 (1.7.3-aarch64-linux) - sqlite3 (1.7.3-arm-linux) - sqlite3 (1.7.3-arm64-darwin) - sqlite3 (1.7.3-x86-linux) - sqlite3 (1.7.3-x86_64-darwin) - sqlite3 (1.7.3-x86_64-linux) - stringio (3.1.0) - strscan (3.1.0) - terminal-table (3.0.2) - unicode-display_width (>= 1.1.1, < 3) - thor (1.3.1) - timeout (0.4.1) - tty-option (0.3.0) - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - unicode-display_width (2.5.0) - uri (0.13.0) - webrick (1.8.2) - websocket-driver (0.7.6) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - zeitwerk (2.6.14) - -PLATFORMS - aarch64-linux - aarch64-linux-gnu - aarch64-linux-musl - arm-linux - arm-linux-gnueabihf - arm-linux-musleabihf - arm64-darwin - x86-linux - x86-linux-gnu - x86-linux-musl - x86_64-darwin - x86_64-linux - x86_64-linux-gnu - x86_64-linux-musl - -DEPENDENCIES - bootsnap - cli-ui (~> 2.2) - debug - dotenv - factory_bot_rails (~> 6.4) - jekyll (~> 4.3) - jekyll-redirect-from (~> 0.16) - minitest-hooks (~> 1.5) - mocha - puma (>= 5.0) - qdrant-ruby - rails (~> 7.1.3, >= 7.1.3.2) - rubocop-shopify - ruby-openai - sqlite3 (~> 1.7) - tty-option (~> 0.3) - tzinfo-data - -BUNDLED WITH - 2.5.3 diff --git a/Makefile b/Makefile deleted file mode 100644 index 28cc4ff7a..000000000 --- a/Makefile +++ /dev/null @@ -1,197 +0,0 @@ -.DEFAULT_GOAL := default - -############################################################################### -# VARIABLES -############################################################################### - -# Paths -DATA_PATH := data -DIST_PATH := dist -DOCS_PATH := docs -DB_PATH := storage -SCHEMA_PATH := schema -TMP_PATH := tmp - -# Data files -CATEGORIES_DATA := $(DATA_PATH)/categories/*.yml -ATTRIBUTES_DATA := $(DATA_PATH)/attributes.yml -VALUES_DATA := $(DATA_PATH)/values.yml - -LOCALIZATION_SOURCES := $(shell find ${DATA_PATH} -maxdepth 2 -type f \( -path "${DATA_PATH}/*" -o -path "${DATA_PATH}/categories/*" \)) - -# Generated files -GENERATED_DOCS_SENTINEL := $(TMP_PATH)/.docs_generated_sentinel -GENERATED_DOCS := $(DOCS_PATH)/_data/unstable - -GENERATED_DIST_SENTINEL := $(TMP_PATH)/.dist_generated_sentinel -GENERATED_LOCALIZATION_SENTINEL := $(TMP_PATH)/.localization_updated_sentinel - -TAXONOMY_JSON := $(DIST_PATH)/en/taxonomy.json -CATEGORIES_JSON := $(DIST_PATH)/en/categories.json -ATTRIBUTES_JSON := $(DIST_PATH)/en/attributes.json -MAPPINGS_JSON := $(DIST_PATH)/en/integrations/all_mappings.json - -DB_DEV := $(DB_PATH)/development.sqlite3 - -# Taxonomy mapping generation tooling -QDRANT_PORT := 6333 -QDRANT_CONTAINER_NAME := qdrant_taxonomy_mappings - -# Input variables -LOCALES ?= en -VERBOSE ?= 0 - -# Formatting helpers -ifeq ($(VERBOSE),1) - V := - VPIPE := - VERBOSE_ARG := --verbose -else - V := @ - VPIPE := > /dev/null - VERBOSE_ARG := -endif - -FMT := printf "\e[%sm>> %-21s\e[0;1m β†’\e[1;32m %s\e[0m\n" -LOG_BUILD := $(FMT) "1;34" # bold blue -LOG_CLEAN := $(FMT) "1;31" # bold red -LOG_ADVISORY := printf "\e[%sm!! %-21s\e[0;1m\n" "1;31" # bold red text with a !! prefix -LOG_CMD := printf "\e[%sm>> %-21s\e[0;1m\n" "1;34" # bold blue text with a >> prefix - -############################################################################### -# TARGETS -############################################################################### - -# Default target -default: build -.PHONY: default - -# Build targets -build: $(GENERATED_DIST_SENTINEL) $(GENERATED_DOCS_SENTINEL) ${GENERATED_LOCALIZATION_SENTINEL} -.PHONY: build - -$(GENERATED_DIST_SENTINEL): $(DB_DEV) - @$(LOG_BUILD) "Building Distribution" "$(DIST_PATH)/*.[json|txt]" - $(V) bin/generate_dist --locales $(LOCALES) $(VERBOSE_ARG) - $(V) touch $@ - -$(GENERATED_DOCS_SENTINEL): $(GENERATED_DIST_SENTINEL) - @$(LOG_BUILD) "Building Docs" "$(GENERATED_DOCS)/*" - $(V) bin/generate_docs $(VERBOSE_ARG) - $(V) touch $@ - -$(GENERATED_LOCALIZATION_SENTINEL): $(LOCALIZATION_SOURCES) - @$(LOG_BUILD) "Syncing English Localizations" - $(V) bin/sync_en_localizations $(VERBOSE_ARG) - $(V) touch $@ - -# Release target -release: $(GENERATED_DIST_SENTINEL) - @$(LOG_CMD) "Preparing release" - $(V) bin/generate_release $(VERBOSE_ARG) -.PHONY: release - -# Clean targets -clean: clean_sentinels clean_dbs clean_docs -.PHONY: clean - -clean_sentinels: - @$(LOG_CLEAN) "Cleaning sentinels" "$(GENERATED_DIST_SENTINEL) $(GENERATED_DOCS_SENTINEL) $(GENERATED_LOCALIZATION_SENTINEL)" - $(V) rm -f $(GENERATED_DIST_SENTINEL) $(GENERATED_DOCS_SENTINEL) $(GENERATED_LOCALIZATION_SENTINEL) -.PHONY: clean_sentinels - -clean_dbs: - @$(LOG_CLEAN) "Cleaning local dbs" $(DB_DEV) - $(V) bin/rails db:drop $(VERBOSE_ARG) -.PHONY: clean_dbs - -clean_docs: - @$(LOG_CLEAN) "Cleaning unstable docs" $(GENERATED_DOCS) - $(V) rm -rf $(GENERATED_DOCS) -.PHONY: clean_docs - -# Command targets -run_docs: $(GENERATED_DOCS_SENTINEL) - @$(LOG_CMD) "Running docs server" - $(V) bundle exec jekyll serve --source $(DOCS_PATH) --destination _site $(VERBOSE_ARG) -.PHONY: run_docs - -console: - @$(LOG_CMD) "Running console with dependencies" - $(V) bin/rails console -.PHONY: console - -# Database setup -seed: vet_schema_data - @$(LOG_BUILD) "Seeding Database" $(DB_DEV) - $(V) bin/rails db:drop $(VERBOSE_ARG) - $(V) bin/rails db:schema:load $(VERBOSE_ARG) - $(V) bin/seed $(VERBOSE_ARG) -.PHONY: seed - -$(DB_DEV): - if [ ! -f $@ ]; then $(MAKE) seed; fi - -# Test targets -test: test_unit test_integration vet_schema -.PHONY: test - -test_unit: - @$(LOG_CMD) "Running Unit Tests" - $(V) bin/rails unit $(filter-out $@,$(MAKECMDGOALS)) -.PHONY: test_unit - -test_integration: - @$(LOG_CMD) "Running Integration Tests" - $(V) bin/rails integration $(filter-out $@,$(MAKECMDGOALS)) -.PHONY: test_integration - -# Schema validation targets -vet_schema: vet_schema_data vet_schema_dist -.PHONY: vet_schema - -vet_schema_data: - @$(LOG_CMD) "Validating $(ATTRIBUTES_DATA) schema" - $(V) cue vet $(SCHEMA_PATH)/data/attributes_schema.cue $(ATTRIBUTES_DATA) - @$(LOG_CMD) "Validating $(CATEGORIES_DATA) schema" - $(V) cue vet $(SCHEMA_PATH)/data/categories_schema.cue -d '#schema' $(CATEGORIES_DATA) - @$(LOG_CMD) "Validating $(VALUES_DATA) schema" - $(V) cue vet $(SCHEMA_PATH)/data/values_schema.cue -d '#schema' $(VALUES_DATA) -.PHONY: vet_schema_data - -vet_schema_dist: - @$(LOG_CMD) "Validating $(ATTRIBUTES_JSON) schema" - $(V) cue vet $(SCHEMA_PATH)/dist/attributes_schema.cue $(ATTRIBUTES_JSON) - @$(LOG_CMD) "Validating $(CATEGORIES_JSON) schema" - $(V) cue vet $(SCHEMA_PATH)/dist/categories_schema.cue $(CATEGORIES_JSON) - @$(LOG_CMD) "Validating $(TAXONOMY_JSON) schema" - $(V) cue vet $(SCHEMA_PATH)/dist/attributes_schema.cue $(TAXONOMY_JSON) - $(V) cue vet $(SCHEMA_PATH)/dist/categories_schema.cue $(TAXONOMY_JSON) - @$(LOG_CMD) "Validating $(MAPPINGS_JSON) schema" - $(V) cue vet $(SCHEMA_PATH)/dist/mappings_schema.cue $(MAPPINGS_JSON) -.PHONY: vet_schema_dist - -generate_mappings: - @$(LOG_CMD) "Starting Qdrant server" - @podman run -d --name $(QDRANT_CONTAINER_NAME) -p $(QDRANT_PORT):$(QDRANT_PORT) qdrant/qdrant > /dev/null 2>&1 || true - @$(LOG_CMD) "Generating missing taxonomy mappings" - @$(V) bin/generate_missing_mappings $(VERBOSE_ARG) - @$(LOG_CMD) "Stopping Qdrant server" - @podman stop $(QDRANT_CONTAINER_NAME) > /dev/null 2>&1 || true - @podman rm $(QDRANT_CONTAINER_NAME) > /dev/null 2>&1 || true -.PHONY: generate_mappings - -# Update the help target to include the new command -help: - @echo "Makefile targets:" - @echo " default: Build the project" - @echo " build: Build distribution and documentation" - @echo " release: Prepare a release" - @echo " clean: Clean all generated files" - @echo " run_docs: Run the documentation server" - @echo " console: Run the application console" - @echo " seed: Seed the database" - @echo " test: Run all tests" - @echo " vet_schema: Validate schemas" - @echo " generate_mappings: Generate missing taxonomy mappings" -.PHONY: help diff --git a/Rakefile b/Rakefile deleted file mode 100644 index d2a78aa25..000000000 --- a/Rakefile +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -# Add your own tasks in files placed in lib/tasks ending in .rake, -# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. - -require_relative "config/application" - -Rails.application.load_tasks diff --git a/app/commands/application_command.rb b/app/commands/application_command.rb deleted file mode 100644 index 00ac95279..000000000 --- a/app/commands/application_command.rb +++ /dev/null @@ -1,113 +0,0 @@ -# frozen_string_literal: true - -require "cli/ui" -require "tty-option" - -class ApplicationCommand - include TTY::Option - include Loggable - - flag :verbose do - desc "Run verbosely" - short "-v" - long "--verbose" - end - - flag :quiet do - desc "Run quietly" - long "--quiet" - end - - flag :force do - desc "Overwrite files if they exist" - short "-f" - long "--force" - end - - flag :help do - desc "Print usage" - short "-h" - long "--help" - end - - class << self - def run(argv = ARGV) - new(argv:, interactive: true).run - end - end - - def initialize(argv: ARGV, interactive: false, **kwargs) - @interactive = interactive - - parse(argv) - params.merge!(kwargs) - - Loggable.log_level = :debug if params[:verbose] - Loggable.log_level = :error if params[:quiet] - - @sys = System.new(force: params[:force]) - @null_spinner = NullSpinner.new(logger) unless interactive? - end - - # Primary entry point for the command - def run - if params[:help] - print(help) - exit - elsif params.errors.any? - puts params.errors.summary - exit(1) - end - - if interactive? - CLI::UI.frame_style = :bracket - CLI::UI::StdoutRouter.enable - end - - execute - ensure - CLI::UI::StdoutRouter.disable - end - - # Invoke command without normal checks - # Use when invoking from another command or in tests - def execute - raise NotImplementedError, "#{self.class}#execute must be implemented" - end - - def interactive? - @interactive - end - - protected - - attr_reader :sys - - def frame(title, **kargs, &) - if interactive? - CLI::UI::Frame.open(title, **kargs, &) - else - logger.info(title) - yield - end - end - - def spinner(title, **kwargs, &) - if interactive? - CLI::UI::Spinner.spin(title, **kwargs, &) - else - logger.info(title) - yield(@null_spinner) - end - end - - class NullSpinner - def initialize(logger) - @logger = logger - end - - def update_title(title) - @logger.info(title) - end - end -end diff --git a/app/commands/generate_dist_command.rb b/app/commands/generate_dist_command.rb deleted file mode 100644 index 814239f69..000000000 --- a/app/commands/generate_dist_command.rb +++ /dev/null @@ -1,152 +0,0 @@ -# frozen_string_literal: true - -class GenerateDistCommand < ApplicationCommand - usage do - no_command - end - - option :version do - desc "Distribution version" - long "--version string" - end - - option :locales do - desc "Locales to generate" - long "--locales list" - default "en" - convert -> { _1.downcase.split(",") } - end - - def execute - setup_options - frame("Validating Data Files") do - validate_data_files - end - frame("Generating distribution files") do - logger.headline("Version: #{params[:version]}") - logger.headline("Locales: #{params[:locales].join(", ")}") - - params[:locales].each { generate_dist_files(_1) } - end - end - - private - - def setup_options - params[:version] ||= sys.read_file("VERSION").strip - if params[:locales].include?("all") - params[:locales] = sys.glob("data/localizations/categories/*.yml").map { File.basename(_1, ".yml") } - end - end - - def validate_data_files - LocalizationsValidator.new.call(params[:locales]) - rescue LocalizationsValidator::LocalizationError => e - logger.info(e.message) - exit(0) - end - - def generate_dist_files(locale) - frame("Generating files for {{bold:#{locale}}}", color: :magenta) do - generate_txt_files(locale) - generate_json_files(locale) - generate_mapping_files(locale) if locale == "en" - end - end - - def generate_txt_files(locale) - frame("Generating txt files") do - ["categories", "attributes", "attribute_values"].each do |type| - spinner("Generating #{type}.txt") do |sp| - txt_data = case type - when "categories" then Category.as_txt(Category.verticals, version: params[:version], locale:) - when "attributes" then Attribute.as_txt(Attribute.base, version: params[:version], locale:) - when "attribute_values" then Value.as_txt(Value.all, version: params[:version], locale:) - end - - sys.write_file!("dist/#{locale}/#{type}.txt") do |file| - file.write(txt_data) - file.write("\n") - end - sp.update_title("Generated #{type}.txt") - end - end - end - end - - def generate_json_files(locale) - frame("Generating json files") do - # cache json data to avoid duplicate work; but lazy initialize to keep CLI snappy - categories_json = nil - attributes_json = nil - - ["categories", "attributes", "taxonomy", "attribute_values"].each do |type| - spinner("Generating #{type}.json") do |sp| - json_data = case type - when "categories" - categories_json ||= Category.as_json(Category.verticals, version: params[:version], locale:) - when "attributes" - attributes_json ||= Attribute.as_json(Attribute.base, version: params[:version], locale:) - when "taxonomy" - categories_json.merge(attributes_json) - when "attribute_values" - Value.as_json(Value.all, version: params[:version], locale:) - end - - sys.write_file!("dist/#{locale}/#{type}.json") do |file| - file.write(JSON.pretty_generate(json_data)) - file.write("\n") - end - sp.update_title("Generated #{type}.json") - end - end - end - end - - def generate_mapping_files(locale) - frame("Generating mapping files") do - spinner("Generating all_mappings.json") do |sp| - sys.write_file!("dist/#{locale}/integrations/all_mappings.json") do |file| - file.write(JSON.pretty_generate(MappingRule.as_json(MappingRule.all, version: params[:version]))) - file.write("\n") - end - sp.update_title("Generated all_mappings.json") - end - - mapping_groups = MappingRule.all.group_by { |record| [record.input_version, record.output_version] } - mapping_groups.each do |_, records| - generate_mapping_group_files(locale, records) - end - end - end - - def generate_mapping_group_files(locale, records) - directory_path = "dist/#{locale}/integrations/#{records.first.integration.name}" - sys.delete_files!(directory_path) - - input_version = records.first.input_version.gsub("/", "_") - output_version = records.first.output_version.gsub("/", "_") - if input_version.include?("-unstable") - input_version = input_version.delete_suffix("-unstable") - end - - if output_version.include?("-unstable") - output_version = output_version.delete_suffix("-unstable") - end - - ["txt", "json"].each do |ext| - spinner("Generating #{input_version}_to_#{output_version}.#{ext}") do |sp| - sys.write_file!("#{directory_path}/#{input_version}_to_#{output_version}.#{ext}") do |file| - data = case ext - when "txt" then MappingRule.as_txt(records, version: params[:version]) - when "json" then JSON.pretty_generate(MappingRule.as_json(records, version: params[:version])) - end - - file.write(data) - file.write("\n") - end - sp.update_title("Generated #{input_version}_to_#{output_version}.#{ext}") - end - end - end -end diff --git a/app/commands/generate_docs_command.rb b/app/commands/generate_docs_command.rb deleted file mode 100644 index 9e0ebc5ec..000000000 --- a/app/commands/generate_docs_command.rb +++ /dev/null @@ -1,131 +0,0 @@ -# frozen_string_literal: true - -class GenerateDocsCommand < ApplicationCommand - UNSTABLE = "unstable" - ATTRIBUTE_KEYS = ["id", "name", "handle"].freeze - VALUE_KEYS = ["id", "name"].freeze - - usage do - no_command - end - - option :version do - desc "Documentation version" - long "--version string" - default UNSTABLE - end - - def execute - setup_options - frame("Generating documentation files") do - logger.headline("Version: #{params[:version]}") - - generate_data_files - generate_release_folder unless params[:version] == UNSTABLE - end - end - - private - - def setup_options - params[:force] = true if params[:version] == UNSTABLE - end - - def generate_data_files - data_target = "docs/_data/#{params[:version]}" - - taxonomy_data = sys.parse_json("dist/en/taxonomy.json") - category_data = taxonomy_data.fetch("verticals") - attribute_data = taxonomy_data.fetch("attributes") - - spinner("Generating sibling groups") do |sp| - sys.write_file("#{data_target}/sibling_groups.yml") do |file| - file.write(Category.as_json_for_docs_siblings(category_data).to_yaml(line_width: -1)) - file.write("\n") - end - sp.update_title("Generated sibling groups") - end - - spinner("Generating category search index") do |sp| - sys.write_file("#{data_target}/search_index.json") do |file| - file.write(JSON.fast_generate(Category.as_json_for_docs_search(category_data))) - file.write("\n") - end - sp.update_title("Generated category search index") - end - - spinner("Generating attributes") do |sp| - sys.write_file("#{data_target}/attributes.yml") do |file| - file.write(generate_extended_attributes(attribute_data).to_yaml(line_width: -1)) - file.write("\n") - end - sp.update_title("Generated attributes") - end - - spinner("Generating mappings") do |sp| - mappings = Docs::Mappings.new - mappings_json = sys.parse_json("dist/en/integrations/all_mappings.json").fetch("mappings") - mappings_data = mappings.reverse_shopify_mapping_rules(mappings_json) - - sys.write_file("#{data_target}/mappings.yml") do |file| - file.write(mappings_data.to_yaml(line_width: -1)) - file.write("\n") - end - sp.update_title("Generated mappings") - end - - spinner("Generating attributes with categories") do |sp| - sys.write_file("#{data_target}/reversed_attributes.yml") do |file| - file.write(Attribute.as_json_for_docs.to_yaml(line_width: -1)) - file.write("\n") - end - sp.update_title("Generated attributes with categories") - end - - spinner("Generating attribute with categories search index") do |sp| - sys.write_file("#{data_target}/attribute_search_index.json") do |file| - file.write(JSON.fast_generate(Attribute.as_json_for_docs_search)) - file.write("\n") - end - sp.update_title("Generated attribute with categories search index") - end - end - - def generate_release_folder - spinner("Generating release folder") do |sp| - sys.write_file("docs/_releases/#{params[:version]}/index.html") do |file| - content = sys.read_file("docs/_releases/_index_template.html") - content.gsub!("TITLE", params[:version].upcase) - content.gsub!("TARGET", params[:version]) - content.gsub!("GH_URL", "https://github.com/Shopify/product-taxonomy/releases/tag/v#{params[:version]}") - file.write(content) - end - sys.write_file("docs/_releases/#{params[:version]}/attributes.html") do |file| - content = sys.read_file("docs/_releases/_attributes_template.html") - content.gsub!("TITLE", params[:version].upcase) - content.gsub!("TARGET", params[:version]) - content.gsub!("GH_URL", "https://github.com/Shopify/product-taxonomy/releases/tag/v#{params[:version]}") - file.write(content) - end - sp.update_title("Generated release folder") - end - end - - def generate_extended_attributes(attribute_data) - result = attribute_data.each_with_object([]) do |attribute, acc| - if attribute["extended_attributes"].any? - attribute["extended_attributes"].each do |extended_attribute| - acc << attribute.slice(*ATTRIBUTE_KEYS).merge( - "handle" => extended_attribute.fetch("handle"), - "extended_name" => extended_attribute.fetch("name"), - "values" => attribute.fetch("values").map { |value| value.slice(*VALUE_KEYS) }, - ) - end - end - acc << attribute.slice(*ATTRIBUTE_KEYS).merge( - "values" => attribute.fetch("values").map { |value| value.slice(*VALUE_KEYS) }, - ) - end - result - end -end diff --git a/app/commands/generate_missing_mappings_command.rb b/app/commands/generate_missing_mappings_command.rb deleted file mode 100644 index dc8aa7402..000000000 --- a/app/commands/generate_missing_mappings_command.rb +++ /dev/null @@ -1,387 +0,0 @@ -# frozen_string_literal: true - -class GenerateMissingMappingsCommand < ApplicationCommand - PREVIOUS_RELEASE = "2024-10-unstable" # TODO: read from git tags - EMBEDDING_MODEL = "text-embedding-3-small" - MAPPING_GRADER_GPT_MODEL = "gpt-4" - SYSTEM_PROMPT = <<~PROMPT - You are a taxonomy mapping expert who evaluates the accuracy of product category mappings between two taxonomies. - Your task is to review and grade the accuracy of the mappings, Yes or No, based on the following criteria: - 1. Mark a mapping as Yes, i.e. correct, if two categories of a mapping are highly relevant to each other and similar - in terms of product type, function, or purpose. - For example: - - "Apparel & Accessories" and "Clothing, Shoes & Jewelry" - - "Apparel & Accessories > Clothing > One-Pieces" and "Clothing, Shoes & Accessories > Women > Women's Clothing > Jumpsuits & Rompers" - 2. Mark a mapping as No, i.e. incorrect, if two categories of a mapping are irrevant to each other - in terms of product type, function, or purpose. - For example: - - "Apparel & Accessories > Clothing > Dresses" and "Clothing, Shoes & Jewelry>Shoe, Jewelry & Watch Accessories" - - "Apparel & Accessories" and "Clothing, Shoes & Jewelry>Luggage & Travel Gear" - Note, the character ">" in a category name indicates the start of a new category level. For example: - "sporting goods > exercise & fitness > cardio equipment"'s ancestor categories are "sporting goods > exercise & fitness" and "sporting goods". - You will receive a list of mappings. Each mapping contains a from_category name and a to_category name. - e.g. user's prompt in json format: - [ - { - "from_category_id": "111", - "from_category": "Apparel & Accessories > Jewelry > Smart Watches", - "to_category_id": "222", - "to_category": "Clothing, Shoes & Jewelry>Men's Fashion>Men's Watches>Men's Smartwatches", - }, - { - "from_category_id": "333", - "from_category": "Apparel & Accessories > Clothing > One-Pieces", - "to_category_id": "444", - "to_category": "Clothing, Shoes & Accessories > Women > Women's Clothing > Outfits & Sets", - }, - ] - You evaluate accuracy of every mapping and reply in the following format. Do not change the order of mappings in your reply. - e.g. your response in json format: - [ - { - "from_category_id": "111", - "from_category": "Apparel & Accessories > Jewelry > Smart Watches", - "to_category_id": "222", - "to_category": "Clothing, Shoes & Jewelry>Men's Fashion>Men's Watches>Men's Smartwatches", - "agree_with_mapping": "Yes", - }, - { - "from_category_id": "333", - "from_category": "Apparel & Accessories > Clothing > One-Pieces", - "to_category_id": "444", - "to_category": "Clothing, Shoes & Accessories > Women > Women's Clothing > Outfits & Sets", - "agree_with_mapping": "No", - }, - ] - PROMPT - - usage do - no_command - end - - environment :openai_api_base do - desc "OpenAI API URL for mappings generation" - required - end - - environment :openai_api_key do - desc "OpenAI API key for mappings generation" - required - end - - environment :qdrant_api_base do - desc "Qdrant API URL for embeddings search" - default "http://localhost:6333" - end - - environment :qdrant_api_key do - desc "Qdrant API key for embeddings search" - end - - option :shopify_version do - desc "Target shopify taxonomy version" - long "--version string" - default PREVIOUS_RELEASE - end - - option :retries do - desc "Number of retries for OpenAI API" - short "-r" - long "--retries integer" - default 3 - convert :int - end - - def execute - frame("Generating missing mappings") do - logger.headline("Target Shopify version: #{params[:version]}") - logger.headline("OpenAI url: #{params[:openai_api_base]}") - logger.headline("Qdrant url: #{params[:qdrant_api_base]}") - - find_unmapped_categories - return if @unmapped_categories_groups.empty? - - generate_missing_mappings - end - end - - private - - def find_unmapped_categories - spinner("Finding Shopify categories that are unmapped") do |sp| - all_shopify_ids = Set.new(Category.all.pluck(:id)) - input_version = "shopify/#{params[:shopify_version]}" - mappings_by_output = MappingRule.where(input_version:).group_by(&:output_version) - - @unmapped_categories_groups = mappings_by_output.filter_map do |output_taxonomy, mappings| - found_shopify_ids = Set.new(mappings.map { _1.input.product_category_id.split("/").last }) - excluded_category_ids = Set.new( - sys.parse_yaml(mapping_file_path(output_taxonomy))["unmapped_product_category_ids"], - ) - unmapped_ids = all_shopify_ids - found_shopify_ids - excluded_category_ids - id_name_map = Category.where(id: unmapped_ids).map { [_1.id, _1.full_name] }.to_h - next if id_name_map.empty? - - { output_taxonomy:, id_name_map: } - end - - sp.update_title("Found #{@unmapped_categories_groups.sum { _1[:id_name_map].size }} unmapped Shopify categories") - end - end - - def generate_missing_mappings - frame("Generating missing mappings") do - @unmapped_categories_groups.each do |unmapped_categories| - output_taxonomy = unmapped_categories[:output_taxonomy] - - frame("Generating mappings to #{output_taxonomy}") do - embedding_data = load_embedding_data(output_taxonomy) - embedding_collection = output_taxonomy.gsub(%r{[/\-]}, "_") - - index_embedding_data(embedding_data:, embedding_collection:) - generate_and_evaluate_mappings_for_group(unmapped_categories:, embedding_collection:) - end - end - end - end - - def load_embedding_data(output_taxonomy) - data = nil - spinner("Loading embeddings") do |sp| - files = sys.glob("data/integrations/#{output_taxonomy}/embeddings/_*.txt") - sp.update_title("Parsing #{files.size} parts") - - data = files.each_with_object({}) do |partition, embedding_data| - sys.read_file(partition).each_line do |line| - word, vector_str = line.chomp.split(":", 2) - vector = vector_str.split(", ").map { BigDecimal(_1).to_f } - embedding_data[word] = vector - end - - sp.update_title("Loaded embeddings") - end - end - data - end - - def index_embedding_data(embedding_data:, embedding_collection:) - spinner("Indexing embeddings") do |sp| - qdrant_client.collections.delete(collection_name: embedding_collection) - qdrant_client.collections.create( - collection_name: embedding_collection, - vectors: { size: 1536, distance: "Cosine" }, - ) - - points = embedding_data.map.with_index do |(key, value), index| - { - id: index + 1, - vector: value, - payload: { embedding_collection => key }, - } - end - - points.each_slice(100) do |batch| - qdrant_client.points.upsert( - collection_name: embedding_collection, - points: batch, - ) - end - sp.update_title("Indexed embeddings") - end - end - - def generate_and_evaluate_mappings_for_group(unmapped_categories:, embedding_collection:) - mapping_data = nil - all_generated_mappings = [] - disagree_messages = [] - - frame("Generating and evaluating mappings for each Shopify category") do - destination_name_id_map = sys.parse_yaml(full_names_path(unmapped_categories)).pluck("full_name", "id").to_h - mapping_data = sys.parse_yaml(mapping_file_path(unmapped_categories[:output_taxonomy])) - - # TODO: parallelize - unmapped_categories[:id_name_map].each do |source_category_id, source_category_name| - generated_mapping = generate_mapping( - source_category_id:, - source_category_name:, - embedding_collection:, - destination_name_id_map:, - ) - - mapping_data["rules"] << generated_mapping[:new_entry] - all_generated_mappings << generated_mapping[:mapping_to_be_graded] - disagree_messages << generated_mapping[:mapping_to_be_graded] if generated_mapping[:grading_result] == "No" - end - end - - spinner("Writing updated mappings to #{mapping_file_path(unmapped_categories[:output_taxonomy])}") do - mapping_data["rules"].sort_by! { Category.id_parts(_1["input"]["product_category_id"]) } - sys.write_file!(mapping_file_path(unmapped_categories[:output_taxonomy])) do |file| - puts "Updating mapping file: #{file.path}" - file.write(mapping_data.to_yaml) - end - end - - write_summary_message( - all_generated_mappings:, - disagree_messages:, - total_count: mapping_data["rules"].size, - current_iteration_count: unmapped_categories[:id_name_map].size, - ) - end - - def full_names_path(unmapped_categories) - "data/integrations/#{unmapped_categories[:output_taxonomy]}/full_names.yml" - end - - def mapping_file_path(output_taxonomy) - "data/integrations/#{output_taxonomy}/mappings/from_shopify.yml" - end - - def generate_mapping(source_category_id:, source_category_name:, embedding_collection:, destination_name_id_map:) - result = nil - spinner("#{source_category_id}: Generating mapping") do |sp| - category_embedding = request_embeddings(source_category_name) - - sp.update_title("#{source_category_id}: Searching vector DB") - top_candidate = search_top_candidate(query_embedding: category_embedding, embedding_collection:) - - destination_category_id = destination_name_id_map[top_candidate].to_s - mapping_to_be_graded = { - from_category_id: source_category_id, - from_category: source_category_name, - to_category_id: destination_category_id, - to_category: top_candidate, - } - sp.update_title("#{source_category_id}: Grading candidate `#{destination_category_id}`") - grading_result = grade_taxonomy_mapping(mapping_to_be_graded) - - new_entry = { - "input" => { "product_category_id" => source_category_id }, - "output" => { "product_category_id" => [destination_category_id] }, - } - - sp.update_title("#{source_category_id}: Mapped to `#{destination_category_id}` with grade `#{grading_result}`") - result = { new_entry:, mapping_to_be_graded:, grading_result: } - end - result - end - - def request_embeddings(category_name) - with_retries do - response = openai_client.embeddings( - parameters: { - model: EMBEDDING_MODEL, - input: category_name, - }, - ) - response.dig("data", 0, "embedding") - end - end - - def search_top_candidate(query_embedding:, embedding_collection:) - result = qdrant_client.points.search( - collection_name: embedding_collection, - vector: query_embedding, - with_payload: true, - limit: 1, - ) - result.dig("result", 0, "payload", embedding_collection) - end - - def grade_taxonomy_mapping(mapping) - with_retries do - response = openai_client.chat( - parameters: { - model: MAPPING_GRADER_GPT_MODEL, - messages: [ - { role: "system", content: SYSTEM_PROMPT }, - { role: "user", content: [mapping].to_json }, - ], - temperature: 0, - }, - ) - JSON.parse(response.dig("choices", 0, "message", "content")).first["agree_with_mapping"] - end - end - - # TODO: This will be overwritten if we are ever generating for more than 1 taxonomy - def write_summary_message( - all_generated_mappings:, - disagree_messages:, - total_count:, - current_iteration_count: - ) - spinner("Writing tmp/summary_message.txt") do - sys.write_file!("tmp/summary_message.txt") do |file| - file.puts "## πŸ€– Automatic Taxonomy Mapping Update" - file.puts "We have identified and created mappings for **#{current_iteration_count}** previously missing categories, resulting in a total of **#{total_count}** mappings. **Please review and confirm their accuracy before proceeding with the merge.**" - file.puts - - if current_iteration_count > 0 - file.puts "### βœ… New Mappings Added" - file.puts "The following mappings were automatically generated and added to this PR:" - file.puts - file.puts "```" - - all_generated_mappings.sort_by { Category.id_parts(_1[:from_category_id]) }.each_with_index do |mapping, index| - from = "#{mapping[:from_category_id]} (#{mapping[:from_category]})" - to = "#{mapping[:to_category_id]} (#{mapping[:to_category]})" - file.puts "β†’ #{from}\nβ‡’ #{to}" - file.puts if index < all_generated_mappings.size - 1 - end - - file.puts "```" - file.puts - - if disagree_messages.present? - file.puts "> [!WARNING]" - file.puts "Some of the generated mappings have been assigned a **low confidence** rating. As a part of your review, please pay special attention to the following mappings:" - file.puts "```" - - disagree_messages.sort_by { Category.id_parts(_1[:from_category_id]) }.each_with_index do |mapping, index| - from = "#{mapping[:from_category_id]} (#{mapping[:from_category]})" - to = "#{mapping[:to_category_id]} (#{mapping[:to_category]})" - file.puts "β†’ #{from}\nβ‡’ #{to}" - file.puts if index < disagree_messages.size - 1 - end - - file.puts "```" - end - end - end - end - end - - def with_retries - retries = 0 - begin - yield - rescue StandardError => e - retries += 1 - if retries <= params[:retries] - logger.debug("Received error: #{e.message}. Retrying (#{retries}/#{params[:retries]})...") - sleep(1) - retry - else - logger.fatal("Failed after #{params[:retries]} retries.") - raise - end - end - end - - def openai_client - @openai_client ||= OpenAI::Client.new( - uri_base: params[:openai_api_base], - access_token: params[:openai_api_key], - request_timeout: 10, - ) - end - - def qdrant_client - @qdrant_client ||= Qdrant::Client.new( - url: params[:qdrant_api_base], - api_key: params[:qdrant_api_key], - ) - end -end diff --git a/app/commands/generate_release_command.rb b/app/commands/generate_release_command.rb deleted file mode 100644 index 04720d3f6..000000000 --- a/app/commands/generate_release_command.rb +++ /dev/null @@ -1,91 +0,0 @@ -# frozen_string_literal: true - -class GenerateReleaseCommand < ApplicationCommand - usage do - no_command - end - - option :version do - desc "Release version" - long "--version VERSION" - end - - def execute - setup_options - frame("Validating Data Files") do - validate_data_files - end - frame("Generating release") do - logger.headline("Version: #{params[:version]}") - - update_version_file if version_mismatch? - generate_dist - generate_docs - prepare_git_tag - update_readme - end - end - - private - - def setup_options - @version_from_file = sys.read_file("VERSION").strip - params[:version] ||= @version_from_file - end - - def validate_data_files - LocalizationsValidator.new.call(params[:locales]) - rescue LocalizationsValidator::LocalizationError => e - logger.fatal(e.message) - exit(1) - end - - def version_mismatch? - params[:version] != @version_from_file - end - - def update_version_file - spinner("Updating VERSION file") do |sp| - sys.write_file!("VERSION") do |file| - file.write(params[:version]) - file.write("\n") - end - sp.update_title("Updated VERSION file to #{params[:version]}") - end - end - - def generate_dist - spinner("Generating distribution files") do |sp| - GenerateDistCommand.new(**params.to_h).execute - sp.update_title("Generated distribution files") - end - end - - def generate_docs - spinner("Generating documentation files") do |sp| - GenerateDocsCommand.new(**params.to_h).execute - sp.update_title("Generated documentation files") - end - end - - def prepare_git_tag - git_tag = "v#{params[:version]}" - spinner("Preparing git tag") do |sp| - system("git", "tag", git_tag) - sp.update_title("Prepared git tag: #{git_tag}") - end - end - - def update_readme - spinner("Updating README.md") do |sp| - content = sys.read_file("dist/README.md") - sys.write_file!("dist/README.md") do |file| - content.gsub!(%r{badge/version-v(?.*?)-blue\.svg}) do |match| - match.sub($LAST_MATCH_INFO[:version], params[:version]) - end - file.write(content) - end - sp.update_title("Updated README.md") - end - end -end diff --git a/app/commands/localizations_validator.rb b/app/commands/localizations_validator.rb deleted file mode 100644 index 8447a135f..000000000 --- a/app/commands/localizations_validator.rb +++ /dev/null @@ -1,101 +0,0 @@ -# frozen_string_literal: true - -class LocalizationsValidator - LOCALIZATION_DIRECTORY = "data/localizations" - CATEGORY_DIRECTORY = "data/categories" - ATTRIBUTES_DIRECTORY = "data/attributes.yml" - VALUES_DIRECTORY = "data/values.yml" - - class LocalizationError < StandardError; end - - def call(locales = nil) - validate_categories(locales) - validate_attributes(locales) - validate_values(locales) - validate_localization_locales - end - - private - - def validate_categories(locales = nil) - localization_files = Dir.glob("#{LOCALIZATION_DIRECTORY}/categories/*.yml") - localization_files.select! { |file| locales.include?(File.basename(file, ".yml")) } if locales - - localization_files.each do |file| - language = File.basename(file, ".yml") - localizations = YAML.load_file(file) - Dir.glob("#{CATEGORY_DIRECTORY}/*.yml").each do |file| - categories = YAML.load_file(file) - - missing_localization_keys = categories.reject do |category| - localizations[language]["categories"][category["id"]] - end - error_message = "Missing localization keys for #{missing_localization_keys.map { _1["id"] }}" - raise LocalizationError, error_message unless missing_localization_keys.empty? - - missing_localizations = categories.reject do |category| - localizations.dig(language, "categories", category["id"], "name") - end - - error_message = "Missing localization names for #{missing_localizations.map { _1["id"] }}" - raise LocalizationError, error_message unless missing_localizations.empty? - end - end - end - - def validate_attributes(locales = nil) - localization_files = Dir.glob("#{LOCALIZATION_DIRECTORY}/attributes/*.yml") - localization_files.select! { |file| locales.include?(File.basename(file, ".yml")) } if locales - - localization_files.each do |file| - language = File.basename(file, ".yml") - localizations = YAML.load_file(file) - attributes = YAML.load_file(ATTRIBUTES_DIRECTORY).values.flatten - - missing_localization_keys = attributes.reject do |attribute| - localizations[language]["attributes"][attribute["friendly_id"]] - end - error_message = "Missing localization keys for #{missing_localization_keys.map { _1["friendly_id"] }}" - raise LocalizationError, error_message unless missing_localization_keys.empty? - - missing_localizations = attributes.reject do |attribute| - localizations.dig(language, "attributes", attribute["friendly_id"], "name") - end - error_message = "Missing localization names for #{missing_localizations.map { _1["friendly_id"] }}" - raise LocalizationError, error_message unless missing_localizations.empty? - end - end - - def validate_values(locales = nil) - localization_files = Dir.glob("#{LOCALIZATION_DIRECTORY}/values/*.yml") - localization_files.select! { |file| locales.include?(File.basename(file, ".yml")) } if locales - - localization_files.each do |file| - language = File.basename(file, ".yml") - localizations = YAML.load_file(file) - values = YAML.load_file(VALUES_DIRECTORY) - - missing_localization_keys = values.reject do |value| - localizations[language]["values"][value["friendly_id"]] - end - error_message = "Missing localization keys for #{missing_localization_keys.map { _1["friendly_id"] }}" - raise LocalizationError, error_message unless missing_localization_keys.empty? - - missing_localizations = values.reject do |value| - localizations.dig(language, "values", value["friendly_id"], "name") - end - error_message = "Missing localization names for #{missing_localizations.map { _1["friendly_id"] }}" - raise LocalizationError, error_message unless missing_localizations.empty? - end - end - - def validate_localization_locales - categories_locales = Dir.glob("#{LOCALIZATION_DIRECTORY}/categories/*.yml").map { File.basename(_1, ".yml") } - attributes_locales = Dir.glob("#{LOCALIZATION_DIRECTORY}/attributes/*.yml").map { File.basename(_1, ".yml") } - values_locales = Dir.glob("#{LOCALIZATION_DIRECTORY}/values/*.yml").map { File.basename(_1, ".yml") } - - error_message = "Not all locales have the same set of localizations" - raise LocalizationError, - error_message unless categories_locales == attributes_locales && attributes_locales == values_locales - end -end diff --git a/app/commands/seed_local_command.rb b/app/commands/seed_local_command.rb deleted file mode 100644 index 3c279ce19..000000000 --- a/app/commands/seed_local_command.rb +++ /dev/null @@ -1,218 +0,0 @@ -# frozen_string_literal: true - -class SeedLocalCommand < ApplicationCommand - PERMITTED_TARGETS = ["taxonomy", "integrations"].freeze - - usage do - no_command - end - - option :targets do - desc "Which systems to sync. Syncs all if not specified." - long "--target list" - convert :list - default PERMITTED_TARGETS.join(",") - validate -> { PERMITTED_TARGETS.include?(_1) } - end - - def execute - setup_options - frame("Seeding database") do - import_taxonomy if params[:targets].include?("taxonomy") - import_integrations if params[:targets].include?("integrations") - validate_import - end - end - - private - - def setup_options - params[:targets] ||= ["taxonomy", "integrations"] - end - - def import_taxonomy - frame("Importing taxonomy") do - import_attributes_and_values - import_categories - end - end - - def import_attributes_and_values - spinner("Importing attributes and values") do |sp| - Attribute.insert_all_from_data(attributes_data["base_attributes"]) - Value.insert_all_from_data(values_data, attributes_data["base_attributes"]) - AttributesValue.insert_all_from_data(attributes_data["base_attributes"]) - - inserted_attributes = Attribute.insert_all_from_data( - attributes_data["extended_attributes"], - returning: ["id", "base_friendly_id"], - ) - AttributesValue.insert_all_from_data(inserted_attributes) - sp.update_title("Imported #{Attribute.count} attributes and #{Value.count} values") - end - end - - def import_categories - spinner("Importing categories") do |sp| - verticals_data.each do |vertical_data| - logger.debug("Importing vertical: #{vertical_data.first.fetch("name")}") - Category.insert_all_from_data(vertical_data) - CategoriesAttribute.insert_all_from_data(vertical_data) - end - sp.update_title("Imported #{Category.verticals.count} verticals with #{Category.count} categories") - end - end - - def import_integrations - frame("Importing integrations") do - spinner("Importing integrations and mapping rules") do |sp| - Integration.insert_all_from_data(integrations_data) - sp.update_title("Imported #{Integration.count} integrations") - end - spinner("Importing mapping rules") do |sp| - mapping_rules_from(mapping_rule_files) - sp.update_title("Imported #{MappingRule.count} mapping rules") - end - end - end - - def validate_import - frame("Validating import") do - validate_counts - validate_models - end - end - - def validate_counts - spinner("Validating counts") do |sp| - errors = [] - errors << "Values count mismatch" if values_data.size != Value.count - errors << "Base attributes count mismatch" if attributes_data["base_attributes"].size != Attribute.base.count - errors << "Extended attributes count mismatch" if attributes_data["extended_attributes"].size != Attribute.extended.count - errors << "Verticals count mismatch" if verticals_data.size != Category.verticals.count - errors << "Categories count mismatch" if verticals_data.sum(&:size) != Category.count - errors << "Integrations count mismatch" if integrations_data.size != Integration.count - - if errors.empty? - sp.update_title("All counts validated successfully") - else - logger.fatal(errors.join(", ")) - exit(1) - end - end - end - - def validate_models - [ - Value, - Attribute, - AttributesValue, - Category, - CategoriesAttribute, - Integration, - MappingRule, - Product, - ].each do |model| - spinner("Validating #{model.name.pluralize}") do |sp| - if model.count.zero? - logger.fatal("No #{model.name.pluralize} found") - exit(1) - elsif model.all.any?(:invalid?) - logger.fatal("Invalid #{model.name.pluralize} found") - exit(1) - else - sp.update_title("#{model.name.pluralize} valid") - end - end - end - end - - # TODO: this needs to be simplified - def mapping_rules_from(data) - mapping_rules = [] - - data.each do |file| - logger.debug("β†’ #{file}") - from_shopify = File.basename(file, ".*").split("_")[0] == "from" - integration_name = Pathname.new(file).each_filename.to_a[-4] - integration_id = Integration.find_by(name: integration_name)&.id - next if integration_id.nil? - - raw_mappings = sys.parse_yaml(file) - full_name_file_dir = File.expand_path("..", File.dirname(file)) - full_names = {} - sys.parse_yaml(File.join(full_name_file_dir, "full_names.yml")).each do |category| - full_names[Category.gid(category["id"])] = category["full_name"] - end - input_type = "ShopifyProduct" - output_type = "#{integration_name.capitalize}Product" - unless from_shopify - input_type, output_type = output_type, input_type - end - rules = raw_mappings["rules"] - - rules.each do |rule| - input_product_category_id = rule["input"]["product_category_id"] - input_product_category_full_name = if from_shopify - Category.find_by(id: input_product_category_id)&.full_name - else - if input_product_category_id.is_a?(Integer) - input_product_category_id = Category.gid(input_product_category_id) - end - full_names[input_product_category_id] - end - input_product = Product.find_or_create_from_data!( - rule["input"], - type: input_type, - full_name: input_product_category_full_name, - ) - output_product_category_id = Array(rule["output"]["product_category_id"]).first - output_product_category_full_name = if from_shopify - unless output_product_category_id.starts_with?("gid") - output_product_category_id = Category.gid(output_product_category_id) - end - full_names[output_product_category_id] - else - Category.find_by(id: output_product_category_id)&.full_name - end - output_product = Product.find_or_create_from_data!( - rule["output"], - type: output_type, - full_name: output_product_category_full_name, - ) - - mapping_rules << { - integration_id: integration_id, - from_shopify: from_shopify, - input_id: input_product.id, - output_id: output_product.id, - input_type: input_type, - output_type: output_type, - input_version: raw_mappings["input_taxonomy"], - output_version: raw_mappings["output_taxonomy"], - } - end - end - MappingRule.insert_all(mapping_rules) - end - - def values_data - @values_data ||= sys.parse_yaml("data/values.yml") - end - - def attributes_data - @attributes_data ||= sys.parse_yaml("data/attributes.yml") - end - - def verticals_data - @verticals_data ||= sys.glob("data/categories/*.yml").map { sys.parse_yaml(_1) } - end - - def integrations_data - @integrations_data ||= sys.parse_yaml("data/integrations/integrations.yml") - end - - def mapping_rule_files - @mapping_rule_files ||= sys.glob("data/integrations/*/*/mappings/*_shopify.yml") - end -end diff --git a/app/commands/source/add_attribute_command.rb b/app/commands/source/add_attribute_command.rb deleted file mode 100644 index 8560c64f6..000000000 --- a/app/commands/source/add_attribute_command.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -module Source - class AddAttributeCommand < ApplicationCommand - usage do - no_command - end - - keyword :name do - desc "Name for the new attribute" - required - end - - keyword :description do - desc "Description for the new attribute" - required - end - - option :values do - long "--values string" - desc "A comma separated list of values to add to the attribute" - end - - option :base_attribute_friendly_id do - long "--base_attribute_friendly_id string" - short "-base string" - desc "Friendly ID of the base attribute to extend" - end - - def execute - frame("Adding new attribute") do - setup! - create_attribute! - update_data_files! - end - end - - private - - def setup! - if params[:base_attribute_friendly_id] - if value_names.any? - logger.fatal("Values are not allowed for extended attributes") - exit(1) - end - @base_attribute = Attribute.find_by(friendly_id: params[:base_attribute_friendly_id]) - if @base_attribute.nil? - logger.fatal("Base attribute `#{params[:base_attribute_friendly_id]}` not found") - exit(1) - end - elsif value_names.empty? - logger.fatal("Values are required for base attributes") - exit(1) - end - end - - def create_attribute! - @attribute = Attribute.find_or_create!( - params[:name], - params[:description], - base_attribute: @base_attribute, - value_names: value_names, - ) - rescue => e - logger.fatal("Failed to create attribute: #{e.message}") - exit(1) - end - - def update_data_files! - DumpAttributesCommand.new(interactive: true, **params.to_h).execute - DumpValuesCommand.new(interactive: true, **params.to_h).execute - SyncEnLocalizationsCommand.new(interactive: true, targets: ["attributes", "values"], **params.to_h).execute - GenerateDocsCommand.new(interactive: true, **params.to_h).execute - end - - def value_names - params[:values]&.split(",")&.map(&:strip) || [] - end - end -end diff --git a/app/commands/source/add_attribute_to_categories_command.rb b/app/commands/source/add_attribute_to_categories_command.rb deleted file mode 100644 index 0378e17f1..000000000 --- a/app/commands/source/add_attribute_to_categories_command.rb +++ /dev/null @@ -1,93 +0,0 @@ -# frozen_string_literal: true - -module Source - class AddAttributeToCategoriesCommand < ApplicationCommand - usage do - no_command - end - - keyword :attribute_id do - desc "Attribute ID for the attribute to be added" - required - end - - keyword :category_ids do - desc "A comma separated list of category ID(s) the attribute will be added to" - required - end - - option :include_descendants do - desc "When set, the attribute will be added to all descendants of the specified categories" - short "-d" - long "--descendants" - end - - def execute - setup! - frame("Adding Attribute") do - add_to_attribute! - update_data_files! - end - end - - private - - def setup! - load_attribute - load_categories - end - - def load_attribute - @attribute = Attribute.find_by(id: params[:attribute_id]) - return @attribute if @attribute - - logger.fatal("Attribute`#{params[:attribute_id]}` not found") - exit(1) - end - - def load_categories - param_ids = params[:category_ids].split(",").map(&:strip) - if params[:include_descendants] - like_conditions = param_ids.map { |id| "id LIKE ?" }.join(" OR ") - like_values = param_ids.map { |id| "#{id}%" } - @categories = Category.where(like_conditions, *like_values) - else - @categories = Category.where(id: param_ids) - end - mapped_ids = @categories.map(&:id) - return @categories if (param_ids - mapped_ids).empty? - - missing_ids = param_ids - mapped_ids - logger.fatal("Category IDs `#{missing_ids.join(",")}` not found") - exit(1) - end - - def add_to_attribute! - spinner("Adding Attributes to Categories") do |sp| - @categories.each do |category| - if category.related_attributes.include?(@attribute) - logger.info("Attribute `#{@attribute.friendly_id}` already exists in category `#{category.name}`") - next - end - - category.related_attributes << @attribute - if category.save - sp.update_title("Added attribute `#{@attribute.friendly_id}` to category `#{category.name}`") - else - logger.fatal("Failed to add attribute: #{category.errors.full_messages.to_sentence}") - exit(1) - end - end - - sp.update_title("Added attribute `#{@attribute.friendly_id}` to #{@categories.size} categories") - end - end - - def update_data_files! - roots = @categories.map(&:root).uniq.map(&:id) - DumpVerticalsCommand.new(verticals: roots, interactive: true, **params.to_h).execute - SyncEnLocalizationsCommand.new(interactive: true, targets: ["categories"], **params.to_h).execute - GenerateDocsCommand.new(interactive: true, **params.to_h).execute - end - end -end diff --git a/app/commands/source/add_category_command.rb b/app/commands/source/add_category_command.rb deleted file mode 100644 index 58e8c6301..000000000 --- a/app/commands/source/add_category_command.rb +++ /dev/null @@ -1,69 +0,0 @@ -# frozen_string_literal: true - -module Source - class AddCategoryCommand < ApplicationCommand - usage do - no_command - end - - keyword :name do - desc "Name for the new category" - required - end - - keyword :parent_id do - desc "Parent category ID for the new category" - required - end - - option :id do - desc "Override the created categories ID" - short "-i" - long "--id string" - validate { _1 =~ Category::ID_REGEX } - end - - def execute - setup! - frame("Adding new category") do - create_category! - update_data_files! - end - end - - private - - def setup! - @parent = Category.find_by(id: params[:parent_id]) - params[:id] ||= @parent.next_child_id - return @parent if @parent - - logger.fatal("Parent category `#{params[:parent_id]}` not found") - exit(1) - end - - def create_category! - spinner("Creating category") do |sp| - @new_category = Category - .create_with(id: params[:id]) - .find_or_create_by( - name: params[:name], - parent_id: params[:parent_id], - ) - - if @new_category.valid? - sp.update_title("Created category `#{@new_category.name}` with id=`#{@new_category.id}`") - else - logger.fatal("Failed to create category: #{new_category.errors.full_messages.to_sentence}") - exit(1) - end - end - end - - def update_data_files! - DumpVerticalsCommand.new(verticals: [@new_category.root.id], interactive: true, **params.to_h).execute - SyncEnLocalizationsCommand.new(interactive: true, targets: ["categories"], **params.to_h).execute - GenerateDocsCommand.new(interactive: true, **params.to_h).execute - end - end -end diff --git a/app/commands/source/add_value_command.rb b/app/commands/source/add_value_command.rb deleted file mode 100644 index 185e25a73..000000000 --- a/app/commands/source/add_value_command.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -module Source - class AddValueCommand < ApplicationCommand - usage do - no_command - end - - keyword :name do - desc "Name for the new value" - required - end - - keyword :attribute_friendly_id do - desc "Friendly ID of the primary attribute to add the value to" - required - end - - def execute - frame("Adding new value") do - setup! - create_value! - update_data_files! - end - end - - private - - def setup! - @primary_attribute = Attribute.find_by(friendly_id: params[:attribute_friendly_id]) - - if @primary_attribute.nil? - logger.fatal("Primary attribute `#{params[:attribute_friendly_id]}` not found") - exit(1) - end - - @extended_attributes = @primary_attribute.extended_attributes - end - - def create_value! - Value.find_or_create_for_attribute!(@primary_attribute, params[:name]) - rescue => e - logger.fatal("Failed to create value: #{e.message}") - exit(1) - end - - def update_data_files! - DumpAttributesCommand.new(interactive: true, **params.to_h).execute - DumpValuesCommand.new(interactive: true, **params.to_h).execute - SyncEnLocalizationsCommand.new(interactive: true, targets: ["values"], **params.to_h).execute - GenerateDocsCommand.new(interactive: true, **params.to_h).execute - - logger.warn( - "Attribute has custom sorting, please ensure your new value is in the right position in data/attributes.yml", - ) if @primary_attribute.manually_sorted? - end - end -end diff --git a/app/commands/source/dump_attributes_command.rb b/app/commands/source/dump_attributes_command.rb deleted file mode 100644 index 99fcfbd56..000000000 --- a/app/commands/source/dump_attributes_command.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -module Source - class DumpAttributesCommand < ApplicationCommand - usage do - no_command - end - - def execute - frame("Dumping attributes") do - update_attributes_file - end - end - - private - - def update_attributes_file - spinner("Updating attributes.yml") do |sp| - sys.write_file!("data/attributes.yml") do |file| - file.write(Attribute.as_json_for_data.to_yaml(line_width: -1)) - end - sp.update_title("Updated data/attributes.yml") - end - end - end -end diff --git a/app/commands/source/dump_values_command.rb b/app/commands/source/dump_values_command.rb deleted file mode 100644 index c15f77581..000000000 --- a/app/commands/source/dump_values_command.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -module Source - class DumpValuesCommand < ApplicationCommand - usage do - no_command - end - - def execute - frame("Dumping values") do - update_values_file - end - end - - private - - def update_values_file - spinner("Updating values.yml") do |sp| - sys.write_file!("data/values.yml") do |file| - file.write(Value.as_json_for_data.to_yaml(line_width: -1)) - end - sp.update_title("Updated data/values.yml") - end - end - end -end diff --git a/app/commands/source/dump_verticals_command.rb b/app/commands/source/dump_verticals_command.rb deleted file mode 100644 index 41a0777b1..000000000 --- a/app/commands/source/dump_verticals_command.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -module Source - class DumpVerticalsCommand < ApplicationCommand - usage do - no_command - end - - option :verticals do - desc "Verticals to export to data/" - long "--verticals list" - convert :list - default -> { Category.verticals.pluck(:id).join(",") } - validate -> { Category.verticals.pluck(:id).include?(_1) } - end - - def execute - frame("Dumping #{params[:verticals].size} verticals") do - params[:verticals].each do |vertical_id| - update_vertical_file(vertical_id) - end - end - end - - private - - def update_vertical_file(vertical_id) - vertical = Category.find_by!(id: vertical_id) - spinner("Updating `#{vertical.name}`") do |sp| - path = "data/categories/#{vertical.friendly_name}.yml" - sys.write_file!(path) do |file| - file.write(vertical.as_json_for_data_with_descendants.to_yaml(line_width: -1)) - end - sp.update_title("Updated `#{path}`") - end - end - end -end diff --git a/app/commands/source/rename_category_command.rb b/app/commands/source/rename_category_command.rb deleted file mode 100644 index 5d4567924..000000000 --- a/app/commands/source/rename_category_command.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -module Source - class RenameCategoryCommand < ApplicationCommand - usage do - no_command - end - - keyword :id do - desc "ID of the target category" - required - validate -> { _1 =~ Category::ID_REGEX } - end - - keyword :new_name do - desc "Name to rename the category to" - required - end - - def execute - frame("Renaming existing category") do - find_category! - update_category! - update_data_files! - end - end - - private - - def find_category! - @category = Category.find_by(id: params[:id]) - @original_handle = @category&.friendly_name - return if @category - - logger.fatal("Category `#{params[:id]}` not found") - exit(1) - end - - def update_category! - spinner("Updating category") do |sp| - original_name = @category.name - if @category.update(name: params[:new_name]) - sp.update_title("Updated category `#{original_name}` to `#{params[:new_name]}`") - else - logger.fatal("Failed to update category: #{category.errors.full_messages.join(", ")}") - exit(1) - end - end - end - - def update_data_files! - if @category.root? - logger.info("Category is a vertical, deleting original data file") - sys.delete_file!("data/categories/#{@original_handle}.yml") - end - - DumpVerticalsCommand.new(verticals: [@category.root.id], interactive: true, **params.to_h).execute - SyncEnLocalizationsCommand.new(interactive: true, targets: ["categories"], **params.to_h).execute - GenerateDocsCommand.new(interactive: true, **params.to_h).execute - end - end -end diff --git a/app/commands/source/reparent_category_command.rb b/app/commands/source/reparent_category_command.rb deleted file mode 100644 index 5f636e6ee..000000000 --- a/app/commands/source/reparent_category_command.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -module Source - class ReparentCategoryCommand < ApplicationCommand - usage do - no_command - end - - keyword :category_id do - desc "The target category ID" - required - validate { _1 =~ Category::ID_REGEX } - end - - keyword :parent_id do - desc "The new parent category ID" - required - validate { _1 =~ Category::ID_REGEX } - end - - def execute - find_category! - find_parent! - - frame("Reparenting category") do - logger.headline("Category: #{@category.name}") - logger.headline("Parent: #{@parent.name}") - - update_category! - update_data_files! - end - end - - private - - def find_category! - @category = Category.find_by(id: params[:category_id]) - return if @category - - logger.fatal("Category `#{params[:category_id]}` not found") - exit(1) - end - - def find_parent! - @parent = Category.find_by(id: params[:parent_id]) - return if @parent - - logger.fatal("Parent category `#{params[:parent_id]}` not found") - exit(1) - end - - def update_category! - spinner("Updating category") do |sp| - @category.reparent_to!(@parent) - @category.reload - - sp.update_title("Updated #{@category.name} to belong to `#{@parent.name}`") - end - rescue Category::ReparentError => e - logger.fatal("Failed to reparent category: #{e.message}") - exit(1) - end - - def update_data_files! - DumpVerticalsCommand.new(interactive: true, verticals: [@category.root.id], **params.to_h).execute - DumpAttributesCommand.new(interactive: true, **params.to_h).execute - SyncEnLocalizationsCommand.new(interactive: true, targets: ["categories", "attributes"], **params.to_h).execute - GenerateDocsCommand.new(interactive: true, **params.to_h).execute - end - end -end diff --git a/app/commands/source/sync_en_localizations_command.rb b/app/commands/source/sync_en_localizations_command.rb deleted file mode 100644 index 51bc32bb0..000000000 --- a/app/commands/source/sync_en_localizations_command.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -# TODO: add params to control what gets synced -module Source - class SyncEnLocalizationsCommand < ApplicationCommand - PERMITTED_TARGETS = ["categories", "attributes", "values"].freeze - - usage do - no_command - end - - option :targets do - desc "Which model types to sync. Syncs all if not specified." - long "--target list" - convert :list - default PERMITTED_TARGETS.join(",") - validate -> { PERMITTED_TARGETS.include?(_1) } - end - - def execute - frame("Syncing EN localizations") do - sync_categories if params[:targets].include?("categories") - sync_attributes if params[:targets].include?("attributes") - sync_values if params[:targets].include?("values") - end - end - - private - - def sync_categories - spinner("Syncing categories") do |sp| - localizations = Category.as_json_for_localization(Category.all) - write_localizations("categories", localizations, sp) - end - end - - def sync_attributes - spinner("Syncing attributes") do |sp| - localizations = Attribute.as_json_for_localization(Attribute.all) - write_localizations("attributes", localizations, sp) - end - end - - def sync_values - spinner("Syncing values") do |sp| - localizations = Value.as_json_for_localization(Value.all) - write_localizations("values", localizations, sp) - end - end - - def write_localizations(type, localizations, sp) - file_path = "data/localizations/#{type}/en.yml" - sys.write_file!(file_path) do |file| - file.puts "# This file is auto-generated. Do not edit directly." - file.write(localizations.to_yaml(line_width: -1)) - end - sp.update_title("Wrote #{type} localizations to #{file_path}") - end - end -end diff --git a/app/commands/source/version_changelog_command.rb b/app/commands/source/version_changelog_command.rb deleted file mode 100644 index 2dd8d862d..000000000 --- a/app/commands/source/version_changelog_command.rb +++ /dev/null @@ -1,141 +0,0 @@ -# frozen_string_literal: true - -module Source - class VersionChangelogCommand < ApplicationCommand - usage do - no_command - end - - option :version do - long "--version string" - desc "The version of the full_names.yml file to compare" - end - - def execute - version = params[:version] || latest_stable_version - frame("Processing categories for version #{version}") do - ensure_exports_directory - delete_previous_exports(version) - process_category_tree - compare_and_export(version) - end - end - - private - - def latest_stable_version - # Get the list of directories and sort them to find the latest version - versions = Dir.glob('data/integrations/shopify/*').select do |f| - File.directory?(f) && f.match(/\d{4}-\d{2}/) - end.map do |f| - File.basename(f) - end.sort - - versions.last - end - - def ensure_exports_directory - unless Dir.exist?('exports') - Dir.mkdir('exports') - puts "Created 'exports' directory in product-taxonomy" - end - end - - def delete_previous_exports(version) - if File.exist?('exports/category_tree.csv') - File.delete('exports/category_tree.csv') - puts "Deleted previous category tree export: exports/category_tree.csv" - end - - changelog_file = "exports/version_changelog_from_#{version}.csv" - if File.exist?(changelog_file) - File.delete(changelog_file) - puts "Deleted previous version change log export: #{changelog_file}" - end - end - - def process_category_tree - categories_file = 'dist/en/categories.txt' - categories_data = File.read(categories_file).lines.map(&:strip) - - CSV.open('exports/category_tree.csv', 'w') do |csv| - csv << ['ID', 'Breadcrumb', 'Vertical Name', 'Category Name'] - categories_data.each do |line| - # Skip comments and empty lines - next if line.start_with?('#') || line.empty? - - if line =~ /^gid:\/\/shopify\/TaxonomyCategory\/(\S+)\s*:\s*(.+)$/ - id = $1 - breadcrumb = $2 - vertical_name = extract_vertical_name(breadcrumb) - category_name = extract_category_name(breadcrumb) - csv << [id, breadcrumb, vertical_name, category_name] - else - logger.warn("Warning: Line format incorrect - #{line}") - end - end - end - - puts "Category data has been written to category_tree.csv" - end - - def compare_and_export(version) - full_names_file = "data/integrations/shopify/#{version}/full_names.yml" - full_names_data = YAML.load_file(full_names_file) - full_names_hash = full_names_data.each_with_object({}) do |entry, hash| - hash[entry['id']] = entry['full_name'] - end - - categories_file = 'dist/en/categories.txt' - categories_data = File.read(categories_file).lines.map(&:strip) - categories_hash = categories_data.each_with_object({}) do |line, hash| - # Skip comments and empty lines - next if line.start_with?('#') || line.empty? - - if line =~ /^gid:\/\/shopify\/TaxonomyCategory\/(\S+)\s*:\s*(.+)$/ - id = $1 - category = $2 - hash[id] = category - else - logger.warn("Warning: Line format incorrect - #{line}") - end - end - - changelog_file = "exports/version_changelog_from_#{version}.csv" - CSV.open(changelog_file, 'w') do |csv| - csv << ['Change Type', 'ID', 'Breadcrumb', 'Vertical Name', 'Category Name', 'Renamed From'] - full_names_hash.each do |id, full_name| - if categories_hash.key?(id) - if full_name != categories_hash[id] - vertical_name = extract_vertical_name(categories_hash[id]) - category_name = extract_category_name(categories_hash[id]) - csv << ['renamed', id, categories_hash[id], vertical_name, category_name, "#{full_name}"] - end - else - vertical_name = extract_vertical_name(full_name) - category_name = extract_category_name(full_name) - csv << ['archived', id, full_name, vertical_name, category_name, ''] - end - end - - categories_hash.each do |id, category| - unless full_names_hash.key?(id) - vertical_name = extract_vertical_name(category) - category_name = extract_category_name(category) - csv << ['new', id, category, vertical_name, category_name, ''] - end - end - end - - puts "Version change log from #{version} has been written to #{changelog_file}" - end - - def extract_vertical_name(breadcrumb) - breadcrumb.split(' > ').first - end - - def extract_category_name(breadcrumb) - breadcrumb.split(' > ').last - end - end -end \ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb deleted file mode 100644 index 4ac8823b0..000000000 --- a/app/controllers/application_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class ApplicationController < ActionController::API -end diff --git a/app/controllers/concerns/.gitkeep b/app/controllers/concerns/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/models/application_record.rb b/app/models/application_record.rb deleted file mode 100644 index 81fab9a36..000000000 --- a/app/models/application_record.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -class ApplicationRecord < ActiveRecord::Base - primary_abstract_class - - class << self - def generate_friendly_id(name) - I18n.transliterate(name) - .downcase - .gsub(%r{[^a-z0-9\s\-_/\.\+#]}, "") - .gsub(/[\s\-\.]+/, "_") - end - - def generate_handle(text) - I18n.transliterate(text) - .downcase - .gsub(%r{[^a-z0-9\s\-_/\+#]}, "") - .gsub("+", "-plus-") - .gsub("#", "-hashtag-") - .gsub("/", "-") - .gsub(/[\s\.]+/, "-") - .gsub("_-_", "-") - .gsub(/(? { where(base_friendly_id: nil) } - scope :extended, -> { where.not(base_friendly_id: nil) } - - has_many :categories_attributes, - dependent: :destroy, - foreign_key: :attribute_friendly_id, - primary_key: :friendly_id, - inverse_of: :related_attribute - has_many :categories, through: :categories_attributes - has_many :attributes_values, - dependent: :destroy, - foreign_key: :attribute_id, - primary_key: :id, - inverse_of: :related_attribute - has_many :values, through: :attributes_values - - belongs_to :base_attribute, - class_name: "Attribute", - foreign_key: :base_friendly_id, - primary_key: :friendly_id, - optional: true - - has_many :extended_attributes, - class_name: "Attribute", - foreign_key: :base_friendly_id, - primary_key: :friendly_id - - validates :name, presence: true - validates :friendly_id, presence: true, uniqueness: true - validates :handle, presence: true - validates :description, presence: true - validate :values_match_base, if: :extended? - - LOCALIZATION_PATH = "data/localizations/attributes/*.yml" - - class << self - # - # `data/` deserialization - - def new_from_data(data) - new(row_from_data(data)) - end - - def insert_all_from_data(data, ...) - insert_all!(Array(data).map { row_from_data(_1) }, ...) - end - - def find_or_create!(name, description, base_attribute: nil, value_names: nil) - raise "Value names are not allowed when extending a base attribute" if base_attribute && value_names&.any? - raise "Value names are required when creating a base attribute" if value_names&.empty? && base_attribute.nil? - - friendly_id = generate_friendly_id(name) - - existing_attribute = find_by(friendly_id: friendly_id) - raise "Attribute already exists" if existing_attribute - - ActiveRecord::Base.transaction do - attribute = Attribute.new( - name: name, - description: description, - friendly_id: friendly_id, - handle: generate_handle(friendly_id), - base_attribute: base_attribute, - ) - - if base_attribute - base_attribute&.values&.each do |value| - attribute.values << value - end - else - value_names.each do |value_name| - Value.find_or_create_for_attribute!(attribute, value_name) - end - end - - attribute.save! - end - end - - def localizations - @localizations ||= Dir.glob(LOCALIZATION_PATH).each_with_object({}) do |file, localizations| - locale = File.basename(file, ".yml") - localizations[locale] = YAML.load_file(file).dig(locale, "attributes") - end - end - - def find_localization(locale, id, key) - localizations.dig(locale, id, key) - end - - # - # `dist/` serialization - - def as_json(attributes, version:, locale: "en") - { - "version" => version, - "attributes" => attributes.map { _1.as_json(locale:) }, - } - end - - def as_txt(attributes, version:, locale: "en") - header = <<~HEADER - # Shopify Product Taxonomy - Attributes: #{version} - # Format: {GID} : {Attribute name} - HEADER - padding = reorder("LENGTH(id) desc").first.gid.size - [ - header, - *attributes.map { _1.as_txt(padding:, locale:) }, - ].join("\n") - end - - # - # `data/` serialization - - def as_json_for_data - { - "base_attributes" => base.reorder(:id).map(&:as_json_for_data), - "extended_attributes" => extended.reorder(:id).map(&:as_json_for_data), - } - end - - # - # `data/localizations/` serialization - - def as_json_for_localization(attributes) - { - "en" => { - "attributes" => attributes.sort_by(&:friendly_id).reduce({}) { _1.merge!(_2.as_json_for_localization) }, - }, - } - end - - # - # `docs/` serialization - def as_json_for_docs(locale: "en") - { - "attributes" => all.map { _1.as_json_for_docs(locale:) }, - } - end - - def as_json_for_docs_search - all.map do |data| - { - "searchIdentifier" => data["handle"], - "title" => data["name"], - "url" => "?attributeHandle=#{data["handle"]}", - "attribute" => { - "handle" => data["handle"], - }, - } - end - end - - private - - def row_from_data(data) - { - "id" => data["id"], - "name" => data["name"], - "handle" => data["handle"], - "description" => data["description"], - "friendly_id" => data["friendly_id"], - "base_friendly_id" => data["values_from"], - "sorting" => data["sorting"], - } - end - end - - def gid - if base? - "gid://shopify/TaxonomyAttribute/#{id}" - else - base_attribute.gid - end - end - - # english ignores localization files, since they're derived from this source - def name(locale: "en") - if locale == "en" - super() - else - self.class.find_localization(locale, friendly_id, "name") || super() - end - end - - def description(locale: "en") - if locale == "en" - super() - else - self.class.find_localization(locale, friendly_id, "description") || super() - end - end - - def base? - base_attribute.nil? - end - - def extended? - !base? - end - - def manually_sorted? - sorting == "custom" - end - - def next_value_position - return unless manually_sorted? - - values.max_by(&:position).position + 1 - end - - def sorted_values(locale: "en") - ValueSorter.sort(values, locale:) - end - - def value_friendly_ids=(friendly_id) - self.values = Value.where(friendly_id:) - end - - # - # `data/` serialization - - def as_json_for_data - if base? - { - "id" => id, - "name" => name, - "description" => description, - "friendly_id" => friendly_id, - "handle" => handle, - "sorting" => sorting, - "values" => sorted_values.map(&:friendly_id), - }.compact - else - { - "name" => name, - "handle" => handle, - "description" => description, - "friendly_id" => friendly_id, - "values_from" => base_friendly_id, - } - end - end - - # - # `data/localizations/` serialization - - def as_json_for_localization - { - friendly_id => { - "name" => name, - "description" => description, - }, - } - end - - # - # `dist/` serialization - - def as_json(locale: "en") - { - "id" => gid, - "name" => name(locale:), - "handle" => handle, - "description" => description(locale:), - "extended_attributes" => extended_attributes.map do - { - "name" => _1.name(locale:), - "handle" => _1.handle, - } - end, - "values" => sorted_values(locale:).map do - { - "id" => _1.gid, - "name" => _1.name(locale:), - "handle" => _1.handle, - } - end, - } - end - - def as_txt(padding: 0, locale: "en") - "#{gid.ljust(padding)} : #{name(locale:)}" - end - - # - # `docs/` serialization - def as_json_for_docs(locale: "en") - { - "id" => gid, - "handle" => handle, - "name" => name(locale:), - "base_name" => base_attribute&.name(locale:), - "categories" => categories.sort_by { _1.full_name(locale:) }.map do - { - "id" => _1.gid, - "full_name" => _1.full_name(locale:), - } - end, - "values" => values.map do - { - "id" => _1.gid, - "name" => _1.name(locale:), - } - end, - } - end - - private - - def values_match_base - return if values == base_attribute.values - - errors.add(:values, "must match base attribute's values") - end -end diff --git a/app/models/attributes_value.rb b/app/models/attributes_value.rb deleted file mode 100644 index bd72e011a..000000000 --- a/app/models/attributes_value.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -class AttributesValue < ApplicationRecord - self.primary_key = [:attribute_id, :value_friendly_id] - - belongs_to :related_attribute, class_name: "Attribute", foreign_key: :attribute_id, primary_key: :id - belongs_to :value, foreign_key: :value_friendly_id, primary_key: :friendly_id - - class << self - # - # `data/` deserialization - - def insert_all_from_data(data, ...) - insert_all!(Array(data).flat_map { rows_from_data(_1) }, ...) - end - - private - - def rows_from_data(data) - values, friendly_id, id = data.values_at("values", "base_friendly_id", "id") - - value_friendly_ids = values || Attribute.find_by!(friendly_id:).values.pluck(:friendly_id) - - value_friendly_ids.map do |value_friendly_id| - { - "attribute_id" => id, - "value_friendly_id" => value_friendly_id, - } - end - end - end -end diff --git a/app/models/categories_attribute.rb b/app/models/categories_attribute.rb deleted file mode 100644 index 03649bfee..000000000 --- a/app/models/categories_attribute.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -class CategoriesAttribute < ApplicationRecord - self.primary_key = [:category_id, :attribute_friendly_id] - - belongs_to :category - belongs_to :related_attribute, class_name: "Attribute", foreign_key: :attribute_friendly_id, primary_key: :friendly_id - - class << self - # - # `data/` deserialization - - def insert_all_from_data(data, ...) - insert_all!(Array(data).flat_map { rows_from_data(_1) }, ...) - end - - private - - def rows_from_data(data) - data["attributes"].map do |friendly_id| - { - "category_id" => data["id"], - "attribute_friendly_id" => friendly_id, - } - end - end - end -end diff --git a/app/models/category.rb b/app/models/category.rb deleted file mode 100644 index d8b9eafe7..000000000 --- a/app/models/category.rb +++ /dev/null @@ -1,350 +0,0 @@ -# frozen_string_literal: true - -class Category < ApplicationRecord - ReparentError = Class.new(StandardError) - - ID_REGEX = /\A[a-z]{2}(-\d+)*\z/ - - default_scope { order(:name) } - - scope :verticals, -> { where(parent_id: nil) } - - belongs_to :parent, class_name: "Category", optional: true - - has_many :children, class_name: "Category", inverse_of: :parent - has_many :categories_attributes, - dependent: :destroy, - foreign_key: :category_id, - primary_key: :id, - inverse_of: :category - has_many :related_attributes, through: :categories_attributes - - validates :id, - presence: { strict: true }, - format: { with: ID_REGEX } - validates :name, - presence: { strict: true } - validate :id_matches_depth - validate :id_starts_with_parent_id, - unless: :root? - validates_associated :children - - LOCALIZATION_PATH = "data/localizations/categories/*.yml" - - class << self - def gid(id) - "gid://shopify/TaxonomyCategory/#{id}" - end - - def id_parts(id) - id.split("-").map { _1 =~ /^\d+$/ ? _1.to_i : _1 } - end - - def localizations - @localizations ||= Dir.glob(LOCALIZATION_PATH).each_with_object({}) do |file, localizations| - locale = File.basename(file, ".yml") - localizations[locale] = YAML.load_file(file).dig(locale, "categories") - end - end - - def find_localization(locale, id, key) - localizations.dig(locale, id, key) - end - - # - # `data/` deserialization - - def new_from_data(data) - new(row_from_data(data)) - end - - def insert_all_from_data(data, ...) - insert_all!(Array(data).map { row_from_data(_1) }, ...) - end - - # - # `dist/` serialization - - def as_json(verticals, version:, locale: "en") - { - "version" => version, - "verticals" => verticals.map { _1.as_json_with_descendants(locale:) }, - } - end - - def as_txt(verticals, version:, locale: "en") - header = <<~HEADER - # Shopify Product Taxonomy - Categories: #{version} - # Format: {GID} : {Ancestor name} > ... > {Category name} - HEADER - padding = reorder("LENGTH(id) desc").first.gid.size - [ - header, - *verticals.flat_map(&:descendants_and_self).map { _1.as_txt(padding:, locale:) }, - ].join("\n") - end - - # - # `data/localizations/` serialization - - def as_json_for_localization(categories) - { - "en" => { - "categories" => categories.sort_by(&:id_parts).reduce({}) { _1.merge!(_2.as_json_for_localization) }, - }, - } - end - - # - # `docs/` parsing - - def as_json_for_docs_siblings(distribution_verticals) - distribution_verticals.each_with_object({}) do |vertical, groups| - vertical["categories"].each do |data| - parent_id = data["parent_id"].presence || "root" - sibling = { - "id" => data["id"], - "name" => data["name"], - "fully_qualified_type" => data["full_name"], - "depth" => data["level"], - "parent_id" => parent_id, - "node_type" => data["level"].zero? ? "root" : "leaf", - "ancestor_ids" => data["ancestors"].map { _1["id"] }.join(","), - "attribute_handles" => data["attributes"].map { _1["handle"] }.join(","), - } - - groups[data["level"]] ||= {} - groups[data["level"]][parent_id] ||= [] - groups[data["level"]][parent_id] << sibling - end - end - end - - def as_json_for_docs_search(distribution_verticals) - distribution_verticals.flat_map do |vertical| - vertical["categories"].map do |data| - { - "searchIdentifier" => data["id"], - "title" => data["full_name"], - "url" => "?categoryId=#{CGI.escapeURIComponent(data["id"])}", - "category" => { - "id" => data["id"], - "name" => data["name"], - "fully_qualified_type" => data["full_name"], - "depth" => data["level"], - }, - } - end - end - end - - private - - def row_from_data(data) - { - "id" => data["id"], - "parent_id" => parent_id_of(data["id"]), - "name" => data["name"], - } - end - - def parent_id_of(id) - id.split("-")[0...-1].join("-").presence - end - end - - def gid - self.class.gid(id) - end - - def id_parts - self.class.id_parts(id) - end - - # english ignores localization files, since they're derived from this source - def name(locale: "en") - if locale == "en" - super() - else - self.class.find_localization(locale, id, "name") || super() - end - end - - def full_name(locale: "en") - ancestors.reverse.map { _1.name(locale:) }.push(name(locale:)).join(" > ") - end - - def friendly_name - "#{id}_#{self.class.generate_friendly_id(name)}" - end - - def root? - parent.nil? - end - - def leaf? - children.empty? - end - - def level - ancestors.size - end - - def root - ancestors.last || self - end - - def ancestors - if root? - [] - else - [parent] + parent.ancestors - end - end - - def ancestors_and_self - [self] + ancestors - end - - def ancestor_of?(category) - descendants.include?(category) - end - - # depth-first given that matches how we want to use this - def descendants - children.flat_map { |child| [child] + child.descendants } - end - - def descendants_and_self - [self] + descendants - end - - def descendant_of?(category) - ancestors.include?(category) - end - - def next_child_id - largest_child_id = children.map { _1.id.split("-").last.to_i }.max || 0 - - "#{id}-#{largest_child_id + 1}" - end - - def reparent_to!(new_parent, new_id: new_parent.next_child_id) - if root? - raise ReparentError, "Cannot reparent a vertical" - elsif new_parent.descendant_of?(self) - raise ReparentError, "new_parent `#{new_parent.name}` is a descendant" - elsif !new_id.start_with?(new_parent.id) - raise ReparentError, "new_id `#{new_id}` is invalid for parent's id `#{new_parent.id}`" - elsif Category.exists?(id: new_id) - raise ReparentError, "new_id `#{new_id}` is already taken" - end - - ActiveRecord::Base.transaction do - original_id = id - - # update self - update_columns(id: new_id, parent_id: new_parent.id) - # update children - Category.where(parent_id: original_id).update_all(parent_id: new_id) - Category.where("id LIKE ?", "#{original_id}-%") - .update_all("id = REPLACE(id, '#{original_id}', '#{new_id}')") - # update attributes - CategoriesAttribute.where(category_id: original_id).update_all(category_id: new_id) - CategoriesAttribute.where("category_id LIKE ?", "#{original_id}-%") - .update_all("category_id = REPLACE(category_id, '#{original_id}', '#{new_id}')") - end - end - - def related_attribute_friendly_ids=(ids) - self.related_attributes = Attribute.where(friendly_id: ids) - end - - # - # `data/` serialization - - def as_json_for_data - { - "id" => id, - "name" => name, - "children" => children.sort_by(&:id_parts).map(&:id), - "attributes" => AlphanumericSorter.sort(related_attributes.map(&:friendly_id), other_last: true), - } - end - - def as_json_for_data_with_descendants - descendants_and_self.sort_by(&:id_parts).map(&:as_json_for_data) - end - - # - # `data/localizations/` serialization - - def as_json_for_localization - { - id => { - "name" => name, - "context" => full_name, - }, - } - end - - # - # `dist/` serialization - - def as_json_with_descendants(locale: "en") - { - "name" => name(locale:), - "prefix" => id.downcase, - "categories" => descendants_and_self.map { _1.as_json(locale:) }, - } - end - - def as_json(locale: "en") - { - "id" => gid, - "level" => level, - "name" => name(locale:), - "full_name" => full_name(locale:), - "parent_id" => parent&.gid, - "attributes" => related_attributes.map do - { - "id" => _1.gid, - "name" => _1.name(locale:), - "handle" => _1.handle, - "description" => _1.description(locale:), - "extended" => _1.extended?, - } - end, - "children" => children.map do - { - "id" => _1.gid, - "name" => _1.name(locale:), - } - end, - "ancestors" => ancestors.map do - { - "id" => _1.gid, - "name" => _1.name(locale:), - } - end, - } - end - - def as_txt(padding: 0, locale:) - "#{gid.ljust(padding)} : #{full_name(locale:)}" - end - - private - - def id_matches_depth - return if id.count("-") == level - - errors.add(:id, "#{id} must have #{level + 1} parts") - end - - def id_starts_with_parent_id - return if id.start_with?(parent.id) - - errors.add(:id, "#{id} must be prefixed by parent_id=#{parent_id}") - end -end diff --git a/app/models/concerns/.gitkeep b/app/models/concerns/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/models/google_product.rb b/app/models/google_product.rb deleted file mode 100644 index 5c5a889bf..000000000 --- a/app/models/google_product.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -class GoogleProduct < Product - store :payload, coder: JSON, accessors: [:product_category_id] -end diff --git a/app/models/integration.rb b/app/models/integration.rb deleted file mode 100644 index fd07258d0..000000000 --- a/app/models/integration.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -class Integration < ApplicationRecord - has_many :mapping_rules - validates :name, presence: true - - serialize :available_versions, type: Array, coder: JSON - - class << self - # - # `data/` deserialization - - def new_from_data(data) - new(row_from_data(data)) - end - - def insert_all_from_data(data, ...) - insert_all!(Array(data).map { row_from_data(_1) }, ...) - end - - private - - def row_from_data(data) - { - "name" => data["name"], - "available_versions" => data["available_versions"], - } - end - end - - def as_json - { - "name" => name, - "available_versions" => available_versions, - } - end - - def as_txt(padding:) - "#{gid.ljust(padding)} : #{name}" - end - - def gid - "gid://shopify/Integration/#{id}" - end -end diff --git a/app/models/mapping_rule.rb b/app/models/mapping_rule.rb deleted file mode 100644 index 5838799ea..000000000 --- a/app/models/mapping_rule.rb +++ /dev/null @@ -1,91 +0,0 @@ -# frozen_string_literal: true - -class MappingRule < ApplicationRecord - belongs_to :integration - belongs_to :input, polymorphic: true - belongs_to :output, polymorphic: true - - PRESENT = "present" - - class << self - # - # `dist/` serialization - - def as_json(mapping_rules, version:) - { - "version" => version, - "mappings" => integration_blocks.map do |source, target| - mapping_blocks_as_json(source, target, mapping_rules) - end, - } - end - - def as_txt(mappings, version:) - header = <<~HEADER - # Shopify Product Taxonomy - Mapping #{mappings.first.input_version} to #{mappings.first.output_version} - # Format: - # β†’ {base taxonomy category name} - # β‡’ {mapped taxonomy category name} - HEADER - visible_mappings = mappings.filter_map do |mapping| - next if mapping.input.type == mapping.output.type && mapping.input.full_name == mapping.output.full_name - - mapping.as_txt.presence - end.sort - [ - header, - *visible_mappings, - ].flatten.join("\n").chomp - end - - private - - def integration_blocks - Integration.all.pluck(:id, :available_versions).flat_map do |id, versions| - _source, _destination = where(integration_id: id).pluck(:input_version, :output_version).uniq - end - end - - def mapping_blocks_as_json(input_version, output_version, mapping_rules) - rules = mapping_rules - .filter_map { _1.as_json if _1.input_version == input_version && _1.output_version == output_version } - .compact_blank - if input_version.include?("shopify") - rules.sort_by! { Category.id_parts(_1["input"]["category"]["id"]) } - end - { - "input_taxonomy" => input_version, - "output_taxonomy" => output_version, - "rules" => rules, - } - end - end - - def as_json - input_integration_version = input_version unless from_shopify? - output_integration_version = output_version if from_shopify? - - input_json = input.as_json(integration_version: input_integration_version) - output_json = output.as_json(integration_version: output_integration_version) - - if input_json.empty? || output_json.empty? - return {} - end - - { - "input" => input_json, - "output" => output_json, - } - end - - def as_txt - input_text = input.as_txt - output_text = output.as_txt - return if input_text.blank? || output_text.blank? - - <<~TEXT - β†’ #{input_text} - β‡’ #{output_text} - TEXT - end -end diff --git a/app/models/product.rb b/app/models/product.rb deleted file mode 100644 index 0c8278349..000000000 --- a/app/models/product.rb +++ /dev/null @@ -1,126 +0,0 @@ -# frozen_string_literal: true - -class Product < ApplicationRecord - has_many :mapping_rules - serialize :payload, coder: JSON - - class << self - # - # `data/` deserialization - - def find_from_data(data, type:) - find_by(type:, payload: payload_for(data, type:)) - end - - def find_or_create_from_data!(data, type:, full_name:) - find_or_create_by!(type:, payload: payload_for(data, type:), full_name:) - end - - def full_name_map - @full_name_map ||= {} - end - - private - - def payload_for(data, type:) - case type - when "ShopifyProduct" - category_id = if data["product_category_id"].is_a?(Array) - data["product_category_id"].map { Category.gid(_1) } - else - Category.gid(data["product_category_id"]) - end - - { - "properties" => data["attributes"], - "product_category_id" => category_id, - } - else - data.slice("product_category_id") - end - end - end - - # - # `data/` serialization - - def as_json_for_data - payload = case product - when ShopifyProduct - { - "product_category_id" => product.product_category_id, - "attributes" => product.properties&.map(&:deep_symbolize_keys), - } - end - { - payload: payload.presence, - type: type, - } - end - - # - # `dist/` serialization - - def as_json(integration_version: nil) - { - "attributes" => get_attributes, - "category" => get_category(integration_version:), - }.compact - end - - def as_txt - full_name.to_s - end - - private - - def get_attributes - payload["properties"]&.map do |property| - { - "attribute" => Attribute.find_by(friendly_id: property["attribute"]).gid, - "value" => Value.find_by(friendly_id: property["value"])&.gid, - } - end - end - - def get_category(integration_version:) - if payload["product_category_id"].is_a?(Array) - payload["product_category_id"].map do |category_id| - get_category_hash(category_id:, integration_version:) - end - else - category_id = parse_category_id(payload["product_category_id"]) - get_category_hash(category_id:, integration_version:) - end - end - - def parse_category_id(category_id) - category_id.split("/").last - end - - def get_category_hash(category_id:, integration_version:) - category_id = parse_category_id(category_id) - if integration_version.nil? - Category.find_by(id: category_id)&.as_json&.slice("id", "full_name") - else - { - "id" => category_id, - "full_name" => integration_full_name(category_id: category_id, integration_version: integration_version), - } - end - end - - def integration_full_name(category_id:, integration_version:) - full_name_map(integration_version:)[category_id.to_s] - end - - def full_name_map(integration_version:) - unless self.class.full_name_map.key?(integration_version) - categories = YAML.load_file(File.join(Rails.root, "data/integrations/#{integration_version}/full_names.yml")) - self.class.full_name_map[integration_version] = categories.each_with_object({}) do |category, hash| - hash[category["id"].to_s] = category["full_name"] - end - end - self.class.full_name_map[integration_version] - end -end diff --git a/app/models/shopify_product.rb b/app/models/shopify_product.rb deleted file mode 100644 index de1b20ad3..000000000 --- a/app/models/shopify_product.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -class ShopifyProduct < Product - store :payload, coder: JSON, accessors: [:product_category_id, :properties] -end diff --git a/app/models/value.rb b/app/models/value.rb deleted file mode 100644 index 8751c637c..000000000 --- a/app/models/value.rb +++ /dev/null @@ -1,197 +0,0 @@ -# frozen_string_literal: true - -class Value < ApplicationRecord - default_scope { order(Arel.sql("CASE WHEN name = 'Other' THEN 1 ELSE 0 END, name")) } - - has_many :attributes_values, - dependent: :destroy, - foreign_key: :value_friendly_id, - primary_key: :friendly_id, - inverse_of: :value - has_many :related_attributes, through: :attributes_values - - belongs_to :primary_attribute, - class_name: "Attribute", - foreign_key: :primary_attribute_friendly_id, - primary_key: :friendly_id - - validates :name, presence: true - validates :friendly_id, presence: true, uniqueness: true - validates :handle, presence: true, uniqueness: { scope: :primary_attribute_friendly_id } - validates :primary_attribute, presence: true - - LOCALIZATION_PATH = "data/localizations/values/*.yml" - - class << self - # - # `data/` deserialization - - def new_from_data(data) - new(row_from_data(data)) - end - - def find_or_create_for_attribute!(attribute, name) - friendly_id = generate_friendly_id("#{attribute.friendly_id}__#{name}") - - existing_value = find_by(friendly_id: friendly_id) - - return existing_value if existing_value - - ActiveRecord::Base.transaction do - value = create!( - name: name, - friendly_id: friendly_id, - handle: generate_handle(friendly_id), - primary_attribute: attribute, - position: attribute.next_value_position, - ) - - attributes = [attribute, *attribute.extended_attributes] - - attributes.each do |attribute| - value.attributes_values.create!(related_attribute: attribute, value:) - end - end - end - - def insert_all_from_data(data, base_attributes_data, ...) - base_attributes_by_friendly_id = base_attributes_data.index_by { _1["friendly_id"] } - - insert_all!(Array(data).map { row_from_data_with_position(_1, base_attributes_by_friendly_id) }, ...) - end - - def localizations - @localizations ||= Dir.glob(LOCALIZATION_PATH).each_with_object({}) do |file, localizations| - locale = File.basename(file, ".yml") - localizations[locale] = YAML.load_file(file).dig(locale, "values") - end - end - - def find_localization(locale, id, key) - localizations.dig(locale, id, key) - end - - # - # `dist/` serialization - - def as_json(values, version:, locale: "en") - { - "version" => version, - "values" => values.map { _1.as_json(locale:) }, - } - end - - def as_txt(values, version:, locale: "en") - header = <<~HEADER - # Shopify Product Taxonomy - Attribute Values: #{version} - # Format: {GID} : {Value name} [{Attribute name}] - HEADER - padding = reorder("LENGTH(id) desc").first.gid.size - [ - header, - *values.map { _1.as_txt(padding: padding, locale:) }, - ].join("\n") - end - - # - # `data/` serialization - - def as_json_for_data - reorder(:id).map(&:as_json_for_data) - end - - # - # `data/localizations/` serialization - - def as_json_for_localization(values) - { - "en" => { - "values" => values.sort_by(&:friendly_id).reduce({}) { _1.merge!(_2.as_json_for_localization) }, - }, - } - end - - private - - def row_from_data_with_position(data, base_attributes_by_friendly_id) - row = row_from_data(data) - - matching_attribute = base_attributes_by_friendly_id[row["primary_attribute_friendly_id"]] - - return row unless matching_attribute["sorting"] == "custom" - - row.merge("position" => matching_attribute["values"].index { _1 == row["friendly_id"] }) - end - - def row_from_data(data) - { - "id" => data["id"], - "name" => data["name"], - "handle" => data["handle"], - "friendly_id" => data["friendly_id"], - "primary_attribute_friendly_id" => data["friendly_id"].split("__").first, - "position" => nil, - } - end - end - - def gid - "gid://shopify/TaxonomyValue/#{id}" - end - - # english ignores localization files, since they're derived from this source - def name(locale: "en") - if locale == "en" - super() - else - self.class.find_localization(locale, friendly_id, "name") || super() - end - end - - def full_name(locale: "en") - "#{name(locale:)} [#{primary_attribute.name(locale:)}]" - end - - def primary_attribute_friendly_id=(friendly_id) - self.primary_attribute = Attribute.find_by(friendly_id:) - end - - # - # `data/` serialization - - def as_json_for_data - { - "id" => id, - "name" => name, - "friendly_id" => friendly_id, - "handle" => handle, - } - end - - # - # `data/localizations/` serialization - - def as_json_for_localization - { - friendly_id => { - "name" => name, - "context" => primary_attribute.name, - }, - } - end - - # - # `dist/` serialization - - def as_json(locale: "en") - { - "id" => gid, - "name" => name(locale:), - "handle" => handle, - } - end - - def as_txt(padding: 0, locale: "en") - "#{gid.ljust(padding)} : #{full_name(locale:)}" - end -end diff --git a/app/models/value_sorter.rb b/app/models/value_sorter.rb deleted file mode 100644 index f49d2ce2c..000000000 --- a/app/models/value_sorter.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -module ValueSorter - class << self - def sort(values, locale: "en") - if values.first.position.present? - values.sort_by(&:position) - else - sort_by_localized_name(values, locale:) - end - end - - private - - def sort_by_localized_name(values, locale:) - values.sort_by.with_index do |value, idx| - [ - value.name(locale: "en").downcase == "other" ? 1 : 0, - *AlphanumericSorter.normalize_value(value.name(locale:)), - idx, - ] - end - end - end -end diff --git a/bin/bundle b/bin/bundle deleted file mode 100755 index 42c7fd7c5..000000000 --- a/bin/bundle +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# -# This file was generated by Bundler. -# -# The application 'bundle' is installed as part of a gem, and -# this file is here to facilitate running it. -# - -require "rubygems" - -m = Module.new do - module_function - - def invoked_as_script? - File.expand_path($0) == File.expand_path(__FILE__) - end - - def env_var_version - ENV["BUNDLER_VERSION"] - end - - def cli_arg_version - return unless invoked_as_script? # don't want to hijack other binstubs - return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` - bundler_version = nil - update_index = nil - ARGV.each_with_index do |a, i| - if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN - bundler_version = a - end - next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ - bundler_version = $1 - update_index = i - end - bundler_version - end - - def gemfile - gemfile = ENV["BUNDLE_GEMFILE"] - return gemfile if gemfile && !gemfile.empty? - - File.expand_path("../Gemfile", __dir__) - end - - def lockfile - lockfile = - case File.basename(gemfile) - when "gems.rb" then gemfile.sub(/\.rb$/, ".locked") - else "#{gemfile}.lock" - end - File.expand_path(lockfile) - end - - def lockfile_version - return unless File.file?(lockfile) - lockfile_contents = File.read(lockfile) - return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ - Regexp.last_match(1) - end - - def bundler_requirement - @bundler_requirement ||= - env_var_version || - cli_arg_version || - bundler_requirement_for(lockfile_version) - end - - def bundler_requirement_for(version) - return "#{Gem::Requirement.default}.a" unless version - - bundler_gem_version = Gem::Version.new(version) - - bundler_gem_version.approximate_recommendation - end - - def load_bundler! - ENV["BUNDLE_GEMFILE"] ||= gemfile - - activate_bundler - end - - def activate_bundler - gem_error = activation_error_handling do - gem "bundler", bundler_requirement - end - return if gem_error.nil? - require_error = activation_error_handling do - require "bundler/version" - end - return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) - warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" - exit 42 - end - - def activation_error_handling - yield - nil - rescue StandardError, LoadError => e - e - end -end - -m.load_bundler! - -if m.invoked_as_script? - load Gem.bin_path("bundler", "bundle") -end diff --git a/bin/copy_docs_search_indexes b/bin/copy_docs_search_indexes deleted file mode 100755 index e2f460928..000000000 --- a/bin/copy_docs_search_indexes +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash - -root=$(dirname $(dirname $0)) - -for dir in $root/docs/_data/*/ -do - src="${dir}search_index.json" - target="$root/_site/releases/$(basename $dir)/search_index.json" - echo "Copying $src to $target" - cp $src $target - - src="${dir}attribute_search_index.json" - target="$root/_site/releases/$(basename $dir)/attribute_search_index.json" - echo "Copying $src to $target" - cp $src $target -done - -exit 0 diff --git a/bin/data/add_attribute b/bin/data/add_attribute deleted file mode 100755 index 7e291f214..000000000 --- a/bin/data/add_attribute +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require_relative "../../config/environment" - -Source::AddAttributeCommand.run diff --git a/bin/data/add_attribute_to_categories b/bin/data/add_attribute_to_categories deleted file mode 100755 index c1ccb7a56..000000000 --- a/bin/data/add_attribute_to_categories +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require_relative "../../config/environment" - -Source::AddAttributeToCategoriesCommand.run diff --git a/bin/data/add_category b/bin/data/add_category deleted file mode 100755 index 7a257a0ad..000000000 --- a/bin/data/add_category +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require_relative "../../config/environment" - -Source::AddCategoryCommand.run diff --git a/bin/data/add_value b/bin/data/add_value deleted file mode 100755 index ff2b583b7..000000000 --- a/bin/data/add_value +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require_relative "../../config/environment" - -Source::AddValueCommand.run diff --git a/bin/data/dump_attributes b/bin/data/dump_attributes deleted file mode 100755 index 7ff021979..000000000 --- a/bin/data/dump_attributes +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require_relative "../../config/environment" - -Source::DumpAttributesCommand.run diff --git a/bin/data/dump_categories b/bin/data/dump_categories deleted file mode 100755 index cd2f72d48..000000000 --- a/bin/data/dump_categories +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require_relative "../../config/environment" - -Source::DumpVerticalsCommand.run diff --git a/bin/data/dump_values b/bin/data/dump_values deleted file mode 100755 index bbce72f30..000000000 --- a/bin/data/dump_values +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require_relative "../../config/environment" - -Source::DumpValuesCommand.run diff --git a/bin/data/rename_category b/bin/data/rename_category deleted file mode 100755 index 448c12653..000000000 --- a/bin/data/rename_category +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require_relative "../../config/environment" - -Source::RenameCategoryCommand.run diff --git a/bin/data/reparent_category b/bin/data/reparent_category deleted file mode 100755 index ac0ec79ba..000000000 --- a/bin/data/reparent_category +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require_relative "../../config/environment" - -Source::ReparentCategoryCommand.run diff --git a/bin/data/version_changelog b/bin/data/version_changelog deleted file mode 100755 index 5748e9883..000000000 --- a/bin/data/version_changelog +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require_relative "../../config/environment" - -Source::VersionChangelogCommand.run \ No newline at end of file diff --git a/bin/generate_dist b/bin/generate_dist deleted file mode 100755 index 8d7e5fb67..000000000 --- a/bin/generate_dist +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require_relative "../config/environment" - -GenerateDistCommand.run diff --git a/bin/generate_docs b/bin/generate_docs deleted file mode 100755 index 2497c8c4b..000000000 --- a/bin/generate_docs +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require_relative "../config/environment" - -GenerateDocsCommand.run diff --git a/bin/generate_missing_mappings b/bin/generate_missing_mappings deleted file mode 100755 index 5fce7045d..000000000 --- a/bin/generate_missing_mappings +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require_relative "../config/environment" - -GenerateMissingMappingsCommand.run diff --git a/bin/generate_release b/bin/generate_release deleted file mode 100755 index 917132e53..000000000 --- a/bin/generate_release +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require_relative "../config/environment" - -GenerateReleaseCommand.run diff --git a/bin/rails b/bin/rails deleted file mode 100755 index efc037749..000000000 --- a/bin/rails +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env ruby -APP_PATH = File.expand_path("../config/application", __dir__) -require_relative "../config/boot" -require "rails/commands" diff --git a/bin/rake b/bin/rake deleted file mode 100755 index e436ea54a..000000000 --- a/bin/rake +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require_relative "../config/boot" -require "rake" -Rake.application.run diff --git a/bin/seed b/bin/seed deleted file mode 100755 index b734094cf..000000000 --- a/bin/seed +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require_relative "../config/environment" - -SeedLocalCommand.run diff --git a/bin/sync_en_localizations b/bin/sync_en_localizations deleted file mode 100755 index 06a5a9921..000000000 --- a/bin/sync_en_localizations +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require_relative "../config/environment" - -Source::SyncEnLocalizationsCommand.run diff --git a/config.ru b/config.ru deleted file mode 100644 index 4a3c09a68..000000000 --- a/config.ru +++ /dev/null @@ -1,6 +0,0 @@ -# This file is used by Rack-based servers to start the application. - -require_relative "config/environment" - -run Rails.application -Rails.application.load_server diff --git a/config/application.rb b/config/application.rb deleted file mode 100644 index e69d69489..000000000 --- a/config/application.rb +++ /dev/null @@ -1,44 +0,0 @@ -require_relative "boot" - -require "rails" -# Pick the frameworks you want: -require "active_model/railtie" -# require "active_job/railtie" -require "active_record/railtie" -# require "active_storage/engine" -require "action_controller/railtie" -# require "action_mailer/railtie" -# require "action_mailbox/engine" -# require "action_text/engine" -require "action_view/railtie" -# require "action_cable/engine" -require "rails/test_unit/railtie" - -# Require the gems listed in Gemfile, including any gems -# you've limited to :test, :development, or :production. -Bundler.require(*Rails.groups) - -module ProductTaxonomy - class Application < Rails::Application - # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 7.1 - - # Please, add to the `ignore` list any other `lib` subdirectories that do - # not contain `.rb` files, or that should not be reloaded or eager loaded. - # Common ones are `templates`, `generators`, or `middleware`, for example. - config.autoload_lib(ignore: %w(assets tasks)) - - # Configuration for the application, engines, and railties goes here. - # - # These settings can be overridden in specific environments using the files - # in config/environments, which are processed later. - # - # config.time_zone = "Central Time (US & Canada)" - # config.eager_load_paths << Rails.root.join("extras") - - # Only loads a smaller set of middleware suitable for API only apps. - # Middleware like session, flash, cookies can be added back manually. - # Skip views, helpers and assets when generating a new resource. - config.api_only = true - end -end diff --git a/config/boot.rb b/config/boot.rb deleted file mode 100644 index aef6d031e..000000000 --- a/config/boot.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) - -require "bundler/setup" # Set up gems listed in the Gemfile. -require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc deleted file mode 100644 index 9d5c60c54..000000000 --- a/config/credentials.yml.enc +++ /dev/null @@ -1 +0,0 @@ -XCgipuTyChfWJfmOMt8ZnrDAFtTXjrjaNeGhkY7niakwMkbLSKdCKNNSk2lFeeuATsYlgghLQmQmkuoOHZfKaMZzMm7IL/DSvTZanaubZdCSDmK5THozlJUeiRdqCgUtGhBKH4vRLoiPYQhOdIPDjnMZXA4W7TcoAK895ghJJj9iHN3QRJsrr7FSvBIqPC0siC5HMEsc/QktNuCFJXcnwlip6G80hwlvfdnZGhwisrQgqGjapAN9altnrKmTL4lEZMndUJalRP/3cYMrigrbs108dNpO3yLppSlZZcCTRqd2o+c8V/xiTHbAl41HQwAjPNZSi71z8drlaSG0cUyKbiUo75HZ/LE95Jue1cfBY2jdhYbMl1dihjWKYD1q9EBgYTwEHUf22AHsCTcoNGn6rCgohYrd--EcsDOaHvEi62rfHr--XIhb0O++KVuP1QVgzCmM5Q== \ No newline at end of file diff --git a/config/database.yml b/config/database.yml deleted file mode 100644 index 796466ba2..000000000 --- a/config/database.yml +++ /dev/null @@ -1,25 +0,0 @@ -# SQLite. Versions 3.8.0 and up are supported. -# gem install sqlite3 -# -# Ensure the SQLite 3 gem is defined in your Gemfile -# gem "sqlite3" -# -default: &default - adapter: sqlite3 - pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> - timeout: 5000 - -development: - <<: *default - database: storage/development.sqlite3 - -# Warning: The database defined as "test" will be erased and -# re-generated from your development database when you run "rake". -# Do not set this db to the same as development or production. -test: - <<: *default - database: storage/test.sqlite3 - -production: - <<: *default - database: storage/production.sqlite3 diff --git a/config/environment.rb b/config/environment.rb deleted file mode 100644 index 7df99e89c..000000000 --- a/config/environment.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -# Load the Rails application. -require_relative "application" - -# Initialize the Rails application. -Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb deleted file mode 100644 index 581687b71..000000000 --- a/config/environments/development.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -require "active_support/core_ext/integer/time" - -Rails.application.configure do - # Settings specified here will take precedence over those in config/application.rb. - - # In the development environment your application's code is reloaded any time - # it changes. This slows down response time but is perfect for development - # since you don't have to restart the web server when you make code changes. - config.enable_reloading = true - - # Do not eager load code on boot. - config.eager_load = false - - # Show full error reports. - config.consider_all_requests_local = true - - # Enable server timing - config.server_timing = true - - # Enable/disable caching. By default caching is disabled. - # Run rails dev:cache to toggle caching. - if Rails.root.join("tmp/caching-dev.txt").exist? - config.cache_store = :memory_store - config.public_file_server.headers = { - "Cache-Control" => "public, max-age=#{2.days.to_i}", - } - else - config.action_controller.perform_caching = false - - config.cache_store = :null_store - end - - # Print deprecation notices to the Rails logger. - config.active_support.deprecation = :log - - # Raise exceptions for disallowed deprecations. - config.active_support.disallowed_deprecation = :raise - - # Tell Active Support which deprecation messages to disallow. - config.active_support.disallowed_deprecation_warnings = [] - - # Raise an error on page load if there are pending migrations. - config.active_record.migration_error = :page_load - - # Highlight code that triggered database queries in logs. - config.active_record.verbose_query_logs = true - - # Raises error for missing translations. - # config.i18n.raise_on_missing_translations = true - - # Annotate rendered view with file names. - # config.action_view.annotate_rendered_view_with_filenames = true - - # Raise error when a before_action's only/except options reference missing actions - config.action_controller.raise_on_missing_callback_actions = true -end diff --git a/config/environments/production.rb b/config/environments/production.rb deleted file mode 100644 index 9b9af6d86..000000000 --- a/config/environments/production.rb +++ /dev/null @@ -1,72 +0,0 @@ -require "active_support/core_ext/integer/time" - -Rails.application.configure do - # Settings specified here will take precedence over those in config/application.rb. - - # Code is not reloaded between requests. - config.enable_reloading = false - - # Eager load code on boot. This eager loads most of Rails and - # your application in memory, allowing both threaded web servers - # and those relying on copy on write to perform better. - # Rake tasks automatically ignore this option for performance. - config.eager_load = true - - # Full error reports are disabled and caching is turned on. - config.consider_all_requests_local = false - - # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment - # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files). - # config.require_master_key = true - - # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. - # config.public_file_server.enabled = false - - # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.asset_host = "http://assets.example.com" - - # Specifies the header that your server uses for sending files. - # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache - # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX - - # Assume all access to the app is happening through a SSL-terminating reverse proxy. - # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. - # config.assume_ssl = true - - # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - config.force_ssl = true - - # Log to STDOUT by default - config.logger = ActiveSupport::Logger.new(STDOUT) - .tap { |logger| logger.formatter = ::Logger::Formatter.new } - .then { |logger| ActiveSupport::TaggedLogging.new(logger) } - - # Prepend all log lines with the following tags. - config.log_tags = [ :request_id ] - - # "info" includes generic and useful information about system operation, but avoids logging too much - # information to avoid inadvertent exposure of personally identifiable information (PII). If you - # want to log everything, set the level to "debug". - config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") - - # Use a different cache store in production. - # config.cache_store = :mem_cache_store - - # Enable locale fallbacks for I18n (makes lookups for any locale fall back to - # the I18n.default_locale when a translation cannot be found). - config.i18n.fallbacks = true - - # Don't log any deprecations. - config.active_support.report_deprecations = false - - # Do not dump schema after migrations. - config.active_record.dump_schema_after_migration = false - - # Enable DNS rebinding protection and other `Host` header attacks. - # config.hosts = [ - # "example.com", # Allow requests from example.com - # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` - # ] - # Skip DNS rebinding protection for the default health check endpoint. - # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } -end diff --git a/config/environments/test.rb b/config/environments/test.rb deleted file mode 100644 index d349c3556..000000000 --- a/config/environments/test.rb +++ /dev/null @@ -1,54 +0,0 @@ -require "active_support/core_ext/integer/time" - -# The test environment is used exclusively to run your application's -# test suite. You never need to work with it otherwise. Remember that -# your test database is "scratch space" for the test suite and is wiped -# and recreated between test runs. Don't rely on the data there! - -Rails.application.configure do - # Settings specified here will take precedence over those in config/application.rb. - - # While tests run files are not watched, reloading is not necessary. - config.enable_reloading = false - - # Eager loading loads your entire application. When running a single test locally, - # this is usually not necessary, and can slow down your test suite. However, it's - # recommended that you enable it in continuous integration systems to ensure eager - # loading is working properly before deploying your code. - config.eager_load = ENV["CI"].present? - - # Configure public file server for tests with Cache-Control for performance. - config.public_file_server.enabled = true - config.public_file_server.headers = { - "Cache-Control" => "public, max-age=#{1.hour.to_i}" - } - - # Show full error reports and disable caching. - config.consider_all_requests_local = true - config.action_controller.perform_caching = false - config.cache_store = :null_store - - # Render exception templates for rescuable exceptions and raise for other exceptions. - config.action_dispatch.show_exceptions = :rescuable - - # Disable request forgery protection in test environment. - config.action_controller.allow_forgery_protection = false - - # Print deprecation notices to the stderr. - config.active_support.deprecation = :stderr - - # Raise exceptions for disallowed deprecations. - config.active_support.disallowed_deprecation = :raise - - # Tell Active Support which deprecation messages to disallow. - config.active_support.disallowed_deprecation_warnings = [] - - # Raises error for missing translations. - # config.i18n.raise_on_missing_translations = true - - # Annotate rendered view with file names. - # config.action_view.annotate_rendered_view_with_filenames = true - - # Raise error when a before_action's only/except options reference missing actions - config.action_controller.raise_on_missing_callback_actions = true -end diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb deleted file mode 100644 index 0c5dd99ac..000000000 --- a/config/initializers/cors.rb +++ /dev/null @@ -1,16 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Avoid CORS issues when API is called from the frontend app. -# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin Ajax requests. - -# Read more: https://github.com/cyu/rack-cors - -# Rails.application.config.middleware.insert_before 0, Rack::Cors do -# allow do -# origins "example.com" -# -# resource "*", -# headers: :any, -# methods: [:get, :post, :put, :patch, :delete, :options, :head] -# end -# end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb deleted file mode 100644 index c2d89e28a..000000000 --- a/config/initializers/filter_parameter_logging.rb +++ /dev/null @@ -1,8 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. -# Use this to limit dissemination of sensitive information. -# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. -Rails.application.config.filter_parameters += [ - :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn -] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb deleted file mode 100644 index 3860f659e..000000000 --- a/config/initializers/inflections.rb +++ /dev/null @@ -1,16 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Add new inflection rules using the following format. Inflections -# are locale specific, and you may define rules for as many different -# locales as you wish. All of these examples are active by default: -# ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.plural /^(ox)$/i, "\\1en" -# inflect.singular /^(ox)en/i, "\\1" -# inflect.irregular "person", "people" -# inflect.uncountable %w( fish sheep ) -# end - -# These inflection rules are supported but not enabled by default: -# ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.acronym "RESTful" -# end diff --git a/config/locales/en.yml b/config/locales/en.yml deleted file mode 100644 index 6c349ae5e..000000000 --- a/config/locales/en.yml +++ /dev/null @@ -1,31 +0,0 @@ -# Files in the config/locales directory are used for internationalization and -# are automatically loaded by Rails. If you want to use locales other than -# English, add the necessary files in this directory. -# -# To use the locales, use `I18n.t`: -# -# I18n.t "hello" -# -# In views, this is aliased to just `t`: -# -# <%= t("hello") %> -# -# To use a different locale, set it with `I18n.locale`: -# -# I18n.locale = :es -# -# This would use the information in config/locales/es.yml. -# -# To learn more about the API, please read the Rails Internationalization guide -# at https://guides.rubyonrails.org/i18n.html. -# -# Be aware that YAML interprets the following case-insensitive strings as -# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings -# must be quoted to be interpreted as strings. For example: -# -# en: -# "yes": yup -# enabled: "ON" - -en: - hello: "Hello world" diff --git a/config/puma.rb b/config/puma.rb deleted file mode 100644 index afa809b43..000000000 --- a/config/puma.rb +++ /dev/null @@ -1,35 +0,0 @@ -# This configuration file will be evaluated by Puma. The top-level methods that -# are invoked here are part of Puma's configuration DSL. For more information -# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. - -# Puma can serve each request in a thread from an internal thread pool. -# The `threads` method setting takes two numbers: a minimum and maximum. -# Any libraries that use thread pools should be configured to match -# the maximum value specified for Puma. Default is set to 5 threads for minimum -# and maximum; this matches the default thread size of Active Record. -max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } -min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } -threads min_threads_count, max_threads_count - -# Specifies that the worker count should equal the number of processors in production. -if ENV["RAILS_ENV"] == "production" - require "concurrent-ruby" - worker_count = Integer(ENV.fetch("WEB_CONCURRENCY") { Concurrent.physical_processor_count }) - workers worker_count if worker_count > 1 -end - -# Specifies the `worker_timeout` threshold that Puma will use to wait before -# terminating a worker in development environments. -worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" - -# Specifies the `port` that Puma will listen on to receive requests; default is 3000. -port ENV.fetch("PORT") { 3000 } - -# Specifies the `environment` that Puma will run in. -environment ENV.fetch("RAILS_ENV") { "development" } - -# Specifies the `pidfile` that Puma will use. -pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } - -# Allow puma to be restarted by `bin/rails restart` command. -plugin :tmp_restart diff --git a/config/routes.rb b/config/routes.rb deleted file mode 100644 index a125ef085..000000000 --- a/config/routes.rb +++ /dev/null @@ -1,10 +0,0 @@ -Rails.application.routes.draw do - # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html - - # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. - # Can be used by load balancers and uptime monitors to verify that the app is live. - get "up" => "rails/health#show", as: :rails_health_check - - # Defines the root path route ("/") - # root "posts#index" -end diff --git a/db/schema.rb b/db/schema.rb deleted file mode 100644 index 2e1bcc098..000000000 --- a/db/schema.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -ActiveRecord::Schema[7.1].define do - create_table :categories, id: :string, force: :cascade do |t| - t.string(:name, null: false) - t.string(:parent_id) - - t.index(:parent_id) - end - create_table :attributes, force: :cascade do |t| - t.string(:name, null: false) - t.string(:friendly_id, null: false) - t.string(:base_friendly_id) - t.string(:handle, null: false) - t.string(:description, null: false) - - t.string(:sorting) - - t.index(:friendly_id, unique: true) - end - create_table :values, force: :cascade do |t| - t.string(:name, null: false) - t.string(:friendly_id, null: false) - t.string(:handle, null: false) - t.string(:primary_attribute_friendly_id) # nullable to avoid cyclic dependency - t.integer(:position) - - t.index(:friendly_id, unique: true) - t.index(:primary_attribute_friendly_id) - end - - create_table :categories_attributes, id: false, force: :cascade do |t| - t.string(:category_id, null: false) - t.string(:attribute_friendly_id, null: false) - - t.index([:category_id, :attribute_friendly_id], unique: true) - t.index(:attribute_friendly_id) - end - create_table :attributes_values, id: false, force: :cascade do |t| - t.integer(:attribute_id, null: false) - t.string(:value_friendly_id, null: false) - - t.index([:attribute_id, :value_friendly_id], unique: true) - t.index(:value_friendly_id) - end - - create_table :integrations, force: :cascade do |t| - t.string(:name, null: false) - t.text(:available_versions) - t.index(:name, unique: true) - end - - create_table :products, force: :cascade do |t| - t.text(:payload) - t.string(:type) - t.string(:full_name) - t.index([:type, :payload, :full_name], unique: true) - end - - create_table :mapping_rules, force: :cascade do |t| - t.integer(:integration_id, null: false) - t.boolean(:from_shopify, default: true) - t.integer(:input_id, null: false) - t.integer(:output_id, null: false) - t.string(:input_type, null: false) - t.string(:output_type, null: false) - t.string(:input_version, null: false) - t.string(:output_version, null: false) - - t.index([:integration_id], name: "index_mapping_rules_on_integration_id") - t.index([:input_id, :output_id], name: "index_unique_mapping_rule", unique: true) - end -end diff --git a/dev.yml b/dev.yml deleted file mode 100644 index 9a4af27e6..000000000 --- a/dev.yml +++ /dev/null @@ -1,44 +0,0 @@ -# dev.yml is a configuration for setting up development environments for Shopify toolchains. -# You may be able to use this tool outside of shopify with Minidev -# See: https://github.com/burke/minidev, but you're probably better off installing ruby manually and using bundle installing -# like any other ruby project. -name: product-taxonomy - -type: ruby - -up: - - ruby - - bundler - - custom: - name: "Remove old cue version" - # Cue versions prior to 0.7.0 won't work but our homebrew integration - # doesn't give us a great way to force an upgrade. - met?: '! ( cue version 2>/dev/null | grep -E "cue version v0\.[123456]\." )' - meet: brew uninstall cue - - node: - version: v20.12.2 - pnpm: 8.15.5 - package_manager: pnpm@8.15.5 - - packages: - - cue - - podman - -commands: - build: - run: make build - rebuild: - run: make clean_sentinels && make build - clean: - run: make clean - console: - run: make console - release: - run: make release - run_docs: - run: make run_docs - seed: - run: make seed - generate_mappings: - run: make generate_mappings - test: - run: make test diff --git a/eslint.config.mjs b/eslint.config.mjs deleted file mode 100644 index 8a20160a9..000000000 --- a/eslint.config.mjs +++ /dev/null @@ -1,7 +0,0 @@ -import globals from "globals"; -import pluginJs from "@eslint/js"; - -export default [ - {languageOptions: { globals: globals.browser }}, - pluginJs.configs.recommended, -]; diff --git a/lib/alphanumeric_sorter.rb b/lib/alphanumeric_sorter.rb deleted file mode 100644 index 6d86271e6..000000000 --- a/lib/alphanumeric_sorter.rb +++ /dev/null @@ -1,113 +0,0 @@ -# frozen_string_literal: true - -module AlphanumericSorter - class << self - def sort(values, other_last: false) - values.sort_by.with_index do |value, idx| - [ - other_last && value.to_s.downcase == "other" ? 1 : 0, - *normalize_value(value), - idx - ] - end - end - - def normalize_value(value) - @normalized_values ||= {} - @normalized_values[value] ||= begin - if (numerical = value.match(RegexPattern::NUMERIC_PATTERN)) - [0, *normalize_numerical(numerical)] - elsif (sequential = value.match(RegexPattern::SEQUENTIAL_TEXT_PATTERN)) - [1, *normalize_sequential(sequential)] - else - [1, normalize_text(value)] - end - end - end - - private - - def normalize_numerical(match) - [ - normalize_text(match[:primary_unit] || match[:secondary_unit]) || "", - normalize_text(match[:seperator]) || "-", - normalize_single_number(match[:primary_number]), - normalize_single_number(match[:secondary_number]), - ] - end - - def normalize_sequential(match) - [ - normalize_text(match[:primary_text]), - normalize_single_number(match[:primary_step]), - normalize_text(match[:primary_unit] || match[:secondary_unit]) || "", - normalize_text(match[:seperator]) || "-", - normalize_text(match[:secondary_text]), - normalize_single_number(match[:secondary_step]), - normalize_text(match[:trailing_text]), - ] - end - - def normalize_single_number(value) - value = value.split.sum(&:to_r) if value&.include?("/") - value.to_f - end - - def normalize_text(value) - return if value.nil? - - ActiveSupport::Inflector.transliterate(value.strip.downcase) - end - end - - module RegexPattern - # matches numbers like -1, 5, 10.5, 3/4, 2 5/8 - SINGLE_NUMBER = %r{ - -? # Optional negative sign - (?: - \d+\.?\d* # Easy numbers like 5, 10.5 - | - (?:\d+\s)?\d+/[1-9]+\d* # Fractions like 3/4, 2 5/8 - ) - }x - - # matches units like sq.ft, km/h - UNITS_OF_MEASURE = %r{ - [^\d\./\-] # Matches any character not a digit, dot, slash or dash - [^\-\d]* # Matches any character not a dash or digit - }x - - # String capturing is simple - BASIC_TEXT = /\D+/ - SEPERATOR = /[\p{Pd}x~]/ - - # NUMERIC_PATTERN matches a primary number with optional units, and an optional range or dimension - # with a secondary number and its optional units. - NUMERIC_PATTERN = %r{ - ^\s*(?#{SINGLE_NUMBER}) # 1. Primary number - \s*(?#{UNITS_OF_MEASURE})? # 2. Optional units for primary number - (?: # Optional range or dimension - \s*(?#{SEPERATOR}) # 3. Separator - \s*(?#{SINGLE_NUMBER}) # 4. Secondary number - \s*(?#{UNITS_OF_MEASURE})? # 5. Optional units for secondary number - )? - \s*$ - }x - - # SEQUENTIAL_TEXT_PATTERN matches a primary non-number string, an optional step, and optional units, - # followed by an optional range or dimension with a secondary non-number string, an optional step, - # and optional units, and finally an optional trailing text. - SEQUENTIAL_TEXT_PATTERN = %r{ - ^\s*(?#{BASIC_TEXT}) # 1. Primary non-number string - \s*(?#{SINGLE_NUMBER})? # 2. Optional step - \s*(?#{UNITS_OF_MEASURE})? # 3. Optional units for primary number - (?: # Optional range or dimension - \s*(?#{SEPERATOR}) # 4. Separator -- capturing allows us to group ranges and dimensions - \s*(?#{BASIC_TEXT})? # 5. Optional secondary non-number string - \s*(?#{SINGLE_NUMBER}) # 6. Secondary step - \s*(?#{UNITS_OF_MEASURE})? # 7. Optional units for secondary number - )? - \s*(?.*)?$ # 8. Optional trailing text - }x - end -end diff --git a/lib/docs/mappings.rb b/lib/docs/mappings.rb deleted file mode 100644 index b48d0fc94..000000000 --- a/lib/docs/mappings.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -module Docs - class Mappings - def reverse_shopify_mapping_rules(mappings) - mappings.each do |mapping| - if shopify_mapping?(mapping) - reverse_taxonomy_names(mapping) - reverse_mapping_rules(mapping) - end - end - end - - def reverse_mapping_rules(mapping) - mapping["rules"].each do |rule| - rule["output"]["category"].each_with_index do |output, index| - if index == 0 - rule["output"] = { "category" => [rule["input"]["category"]] } - rule["input"] = { "category" => output } - else - mapping.push = build_rule(input: output, output: rule["input"]["category"]) - end - end - end - end - - def reverse_taxonomy_names(mapping) - input = mapping["input_taxonomy"] - output = mapping["output_taxonomy"] - mapping["input_taxonomy"] = output - mapping["output_taxonomy"] = input - end - - def shopify_mapping?(mapping) - mapping["output_taxonomy"].include?("shopify") - end - - def build_rule(input:, output:) - { - "input" => { "category" => input }, - "output" => { "category" => [output] }, - } - end - end -end diff --git a/lib/loggable.rb b/lib/loggable.rb deleted file mode 100644 index 69e18b2c2..000000000 --- a/lib/loggable.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -require "cli/ui" - -module Loggable - class CLIUILogger - LEVELS = [:debug, :info, :warn, :error, :fatal] - - def initialize - @level = :info - end - - LEVELS.each do |level| - define_method(level) do |message| - log(level, message) if should_log?(level) - end - end - - def success(message) - CLI::UI.puts(CLI::UI.fmt("{{v}} #{message}")) if should_log?(:info) - end - - def headline(message) - CLI::UI.puts(CLI::UI.fmt("{{*}} #{message}")) if should_log?(:info) - end - - def level=(new_level) - @level = new_level if LEVELS.include?(new_level) - end - - private - - def should_log?(message_level) - LEVELS.index(message_level) >= LEVELS.index(@level) - end - - def log(level, message) - color = case level - when :debug then "blue" - when :info then "green" - when :warn then "yellow" - when :error, :fatal then "red" - end - - CLI::UI.puts(CLI::UI.fmt("[{{#{color}:#{level.upcase}}}] #{message}")) - end - end - - @logger = CLIUILogger.new - - class << self - attr_reader :logger - - def log_level=(level) - @logger.level = level - end - end - - def logger - Loggable.logger - end -end diff --git a/lib/system.rb b/lib/system.rb deleted file mode 100644 index 00305008d..000000000 --- a/lib/system.rb +++ /dev/null @@ -1,107 +0,0 @@ -# frozen_string_literal: true - -require "fileutils" -require "json" -require "optparse" -require "yaml" - -# TODO: Rename to ??? to avoid collision with `system` method -class System - include Loggable - - ROOT = File.expand_path("..", __dir__) - private_constant :ROOT - - class << self - def root - @root ||= Pathname.new(ROOT) - end - - def path(path_from_root) - root.join(path_from_root).relative_path_from(Pathname.pwd) - end - end - - attr_reader :force - - def initialize(force: false) - @force = force - end - - def read_file(path_from_root) - path = path_for(path_from_root) - logger.debug("β†’ Reading `#{path}`") - - File.read(path) - end - - def glob(path_from_root) - path = path_for(path_from_root) - logger.debug("β†’ Globbing `#{path}`") - - Dir.glob(path) - end - - def parse_json(path_from_root) - JSON.parse(read_file(path_from_root)) - end - - def parse_yaml(path_from_root) - YAML.load(read_file(path_from_root)) - end - - def write_file(path_from_root, &) - path = path_for(path_from_root) - if new_or_forced?(path) - write_file!(path, &) - else - logger.debug("β†’ Skipping `#{path}`") - end - end - - def write_file!(path_from_root, &file_block) - path = path_for(path_from_root) - logger.debug("β†’ Writing `#{path}`") - - FileUtils.mkdir_p(File.dirname(path)) - File.open(path, "w", &file_block) - logger.success("Wrote `#{path}`") - end - - def move_file!(target_from_root, new_path_from_root) - target = path_for(target_from_root) - path = path_for(new_path_from_root) - logger.debug("β†’ Moving `#{target}` to `#{path}`") - - FileUtils.mkdir_p(File.dirname(path)) - File.rename(target, path) - logger.success("Moved `#{target}` to `#{path}`") - end - - def delete_file!(path_from_root) - logger.debug("β†’ Deleting `#{path_from_root}`") - File.delete(path_for(path_from_root)) - logger.success("Deleted `#{path_from_root}`") - end - - def delete_files!(directory_path_from_root) - logger.debug("β†’ Deleting files under directory `#{directory_path_from_root}`") - FileUtils.rm_rf(Dir.glob(path_for("#{directory_path_from_root}/*"))) - logger.success("Deleted files under directory`#{directory_path_from_root}`") - end - - private - - def new_or_forced?(path_from_root) - force || !File.exist?(path_for(path_from_root)) - end - - def path_for(from_root_or_path) - case from_root_or_path - when Pathname - from_root_or_path - else - self.class.path(from_root_or_path) - end - end -end diff --git a/lib/tasks/.gitkeep b/lib/tasks/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/lib/tasks/integration.rake b/lib/tasks/integration.rake deleted file mode 100644 index 1291a8728..000000000 --- a/lib/tasks/integration.rake +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -require "rake/testtask" - -desc "Run integration tests" -Rake::TestTask.new(:integration) do |t| - t.libs << "test" - t.pattern = "test/integration/**/*_test.rb" -end diff --git a/lib/tasks/unit.rake b/lib/tasks/unit.rake deleted file mode 100644 index 40efe0c7a..000000000 --- a/lib/tasks/unit.rake +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -require "rake/testtask" - -desc "Run unit tests" -Rake::TestTask.new(:unit) do |t| - t.libs << "test" - t.pattern = "test/models/**/*_test.rb" -end diff --git a/package.json b/package.json deleted file mode 100644 index ad0f6cdcb..000000000 --- a/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "devDependencies": { - "@eslint/js": "^9.3.0", - "eslint": "9.x", - "globals": "^15.3.0", - "prettier": "3.2.5" - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index 713456e66..000000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,616 +0,0 @@ -lockfileVersion: '6.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -devDependencies: - '@eslint/js': - specifier: ^9.3.0 - version: 9.4.0 - eslint: - specifier: 9.x - version: 9.4.0 - globals: - specifier: ^15.3.0 - version: 15.3.0 - prettier: - specifier: 3.2.5 - version: 3.2.5 - -packages: - - /@eslint-community/eslint-utils@4.4.0(eslint@9.4.0): - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - dependencies: - eslint: 9.4.0 - eslint-visitor-keys: 3.4.3 - dev: true - - /@eslint-community/regexpp@4.10.1: - resolution: {integrity: sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - dev: true - - /@eslint/config-array@0.15.1: - resolution: {integrity: sha512-K4gzNq+yymn/EVsXYmf+SBcBro8MTf+aXJZUphM96CdzUEr+ClGDvAbpmaEK+cGVigVXIgs9gNmvHAlrzzY5JQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dependencies: - '@eslint/object-schema': 2.1.3 - debug: 4.3.5 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - dev: true - - /@eslint/eslintrc@3.1.0: - resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dependencies: - ajv: 6.12.6 - debug: 4.3.5 - espree: 10.0.1 - globals: 14.0.0 - ignore: 5.3.1 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - dev: true - - /@eslint/js@9.4.0: - resolution: {integrity: sha512-fdI7VJjP3Rvc70lC4xkFXHB0fiPeojiL1PxVG6t1ZvXQrarj893PweuBTujxDUFk0Fxj4R7PIIAZ/aiiyZPZcg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dev: true - - /@eslint/object-schema@2.1.3: - resolution: {integrity: sha512-HAbhAYKfsAC2EkTqve00ibWIZlaU74Z1EHwAjYr4PXF0YU2VEA1zSIKSSpKszRLRWwHzzRZXvK632u+uXzvsvw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dev: true - - /@humanwhocodes/module-importer@1.0.1: - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - dev: true - - /@humanwhocodes/retry@0.3.0: - resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==} - engines: {node: '>=18.18'} - dev: true - - /@nodelib/fs.scandir@2.1.5: - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - dev: true - - /@nodelib/fs.stat@2.0.5: - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - dev: true - - /@nodelib/fs.walk@1.2.8: - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.17.1 - dev: true - - /acorn-jsx@5.3.2(acorn@8.11.3): - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - acorn: 8.11.3 - dev: true - - /acorn@8.11.3: - resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: true - - /ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - dev: true - - /ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - dev: true - - /ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - dependencies: - color-convert: 2.0.1 - dev: true - - /argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: true - - /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: true - - /brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - dev: true - - /callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - dev: true - - /chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - dev: true - - /color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - dependencies: - color-name: 1.1.4 - dev: true - - /color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: true - - /concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: true - - /cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - dev: true - - /debug@4.3.5: - resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - dev: true - - /deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - dev: true - - /escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - dev: true - - /eslint-scope@8.0.1: - resolution: {integrity: sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - dev: true - - /eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - - /eslint-visitor-keys@4.0.0: - resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dev: true - - /eslint@9.4.0: - resolution: {integrity: sha512-sjc7Y8cUD1IlwYcTS9qPSvGjAC8Ne9LctpxKKu3x/1IC9bnOg98Zy6GxEJUfr1NojMgVPlyANXYns8oE2c1TAA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - hasBin: true - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.4.0) - '@eslint-community/regexpp': 4.10.1 - '@eslint/config-array': 0.15.1 - '@eslint/eslintrc': 3.1.0 - '@eslint/js': 9.4.0 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.3.0 - '@nodelib/fs.walk': 1.2.8 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.5 - escape-string-regexp: 4.0.0 - eslint-scope: 8.0.1 - eslint-visitor-keys: 4.0.0 - espree: 10.0.1 - esquery: 1.5.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.1 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - is-path-inside: 3.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.4 - strip-ansi: 6.0.1 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color - dev: true - - /espree@10.0.1: - resolution: {integrity: sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - dependencies: - acorn: 8.11.3 - acorn-jsx: 5.3.2(acorn@8.11.3) - eslint-visitor-keys: 4.0.0 - dev: true - - /esquery@1.5.0: - resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} - engines: {node: '>=0.10'} - dependencies: - estraverse: 5.3.0 - dev: true - - /esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - dependencies: - estraverse: 5.3.0 - dev: true - - /estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - dev: true - - /esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - dev: true - - /fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true - - /fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - dev: true - - /fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - dev: true - - /fastq@1.17.1: - resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} - dependencies: - reusify: 1.0.4 - dev: true - - /file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} - dependencies: - flat-cache: 4.0.1 - dev: true - - /find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - dev: true - - /flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} - dependencies: - flatted: 3.3.1 - keyv: 4.5.4 - dev: true - - /flatted@3.3.1: - resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} - dev: true - - /glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - dependencies: - is-glob: 4.0.3 - dev: true - - /globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} - dev: true - - /globals@15.3.0: - resolution: {integrity: sha512-cCdyVjIUVTtX8ZsPkq1oCsOsLmGIswqnjZYMJJTGaNApj1yHtLSymKhwH51ttirREn75z3p4k051clwg7rvNKA==} - engines: {node: '>=18'} - dev: true - - /has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - dev: true - - /ignore@5.3.1: - resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} - engines: {node: '>= 4'} - dev: true - - /import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - dev: true - - /imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - dev: true - - /is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - dev: true - - /is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - dependencies: - is-extglob: 2.1.1 - dev: true - - /is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - dev: true - - /isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: true - - /js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - dependencies: - argparse: 2.0.1 - dev: true - - /json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - dev: true - - /json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - dev: true - - /json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - dev: true - - /keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - dependencies: - json-buffer: 3.0.1 - dev: true - - /levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - dev: true - - /locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - dependencies: - p-locate: 5.0.0 - dev: true - - /lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - dev: true - - /minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - dependencies: - brace-expansion: 1.1.11 - dev: true - - /ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: true - - /natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - dev: true - - /optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - dev: true - - /p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - dependencies: - yocto-queue: 0.1.0 - dev: true - - /p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - dependencies: - p-limit: 3.1.0 - dev: true - - /parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - dependencies: - callsites: 3.1.0 - dev: true - - /path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - dev: true - - /path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - dev: true - - /prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - dev: true - - /prettier@3.2.5: - resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} - engines: {node: '>=14'} - hasBin: true - dev: true - - /punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - dev: true - - /queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - dev: true - - /resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - dev: true - - /reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - dev: true - - /run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - dependencies: - queue-microtask: 1.2.3 - dev: true - - /shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - dependencies: - shebang-regex: 3.0.0 - dev: true - - /shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - dev: true - - /strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - dependencies: - ansi-regex: 5.0.1 - dev: true - - /strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - dev: true - - /supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - dependencies: - has-flag: 4.0.0 - dev: true - - /text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - dev: true - - /type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - dependencies: - prelude-ls: 1.2.1 - dev: true - - /uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - dependencies: - punycode: 2.3.1 - dev: true - - /which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - dependencies: - isexe: 2.0.0 - dev: true - - /word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - dev: true - - /yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - dev: true diff --git a/public/robots.txt b/public/robots.txt deleted file mode 100644 index c19f78ab6..000000000 --- a/public/robots.txt +++ /dev/null @@ -1 +0,0 @@ -# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file diff --git a/test/controllers/.gitkeep b/test/controllers/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/factories/attribute_factory.rb b/test/factories/attribute_factory.rb deleted file mode 100644 index d6b5dad0b..000000000 --- a/test/factories/attribute_factory.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -FactoryBot.define do - factory :attribute do - sequence(:name, 1) { "Attribute#{_1}" } - handle { name.downcase } - sequence(:friendly_id) { handle } - description { "Description for #{name}" } - end -end diff --git a/test/factories/category_factory.rb b/test/factories/category_factory.rb deleted file mode 100644 index aaf3b19f0..000000000 --- a/test/factories/category_factory.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -FactoryBot.define do - factory :category do - sequence(:id, 1) do - if parent.nil? - ("a".."z").to_a.sample(2).join - else - "#{parent.id}-#{_1}" - end - end - name { "Category #{id}" } - end -end diff --git a/test/factories/google_product_factory.rb b/test/factories/google_product_factory.rb deleted file mode 100644 index 8506a1e77..000000000 --- a/test/factories/google_product_factory.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -FactoryBot.define do - factory :google_product do - end -end diff --git a/test/factories/integration_factory.rb b/test/factories/integration_factory.rb deleted file mode 100644 index 19b03396f..000000000 --- a/test/factories/integration_factory.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -FactoryBot.define do - factory :integration do - sequence(:name, 1) { "Integration#{_1}" } - end -end diff --git a/test/factories/mapping_rule_factory.rb b/test/factories/mapping_rule_factory.rb deleted file mode 100644 index 23ac77ef2..000000000 --- a/test/factories/mapping_rule_factory.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -FactoryBot.define do - factory :mapping_rule do - association :integration, factory: :integration - association :input, factory: :product - association :output, factory: :product # TODO: FactoryBot is upset about this - - sequence(:input_id, 1) - sequence(:output_id, 1) - input_type { "Product" } - output_type { "GoogleProduct" } - input_version { "shopify/v1" } - output_version { "google/v1" } - end -end diff --git a/test/factories/product_factory.rb b/test/factories/product_factory.rb deleted file mode 100644 index 0ea34cfba..000000000 --- a/test/factories/product_factory.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -FactoryBot.define do - factory :product do - end -end diff --git a/test/factories/value_factory.rb b/test/factories/value_factory.rb deleted file mode 100644 index cad542b5b..000000000 --- a/test/factories/value_factory.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -FactoryBot.define do - factory :value do - association :primary_attribute, factory: :attribute - - sequence(:name, 1) { "Value#{_1}" } - handle { "#{primary_attribute.handle}-#{name.downcase}" } - sequence(:friendly_id) { "#{primary_attribute.handle}__#{name.downcase}" } - end -end diff --git a/test/fixtures/files/.gitkeep b/test/fixtures/files/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/integration/.gitkeep b/test/integration/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/integration/all_data_files_import_test.rb b/test/integration/all_data_files_import_test.rb deleted file mode 100644 index baabdc9eb..000000000 --- a/test/integration/all_data_files_import_test.rb +++ /dev/null @@ -1,176 +0,0 @@ -# frozen_string_literal: true - -require_relative "../test_helper" - -class AllDataFilesImportTest < ActiveSupport::TestCase - include Minitest::Hooks - parallelize(workers: 1) # disable parallelization - - def before_all - Value.delete_all - Attribute.delete_all - Category.delete_all - AttributesValue.delete_all - CategoriesAttribute.delete_all - Integration.delete_all - MappingRule.delete_all - - sys = System.new - @raw_values_data = sys.parse_yaml("data/values.yml") - @raw_attributes_data = sys.parse_yaml("data/attributes.yml") - @raw_verticals_data = sys.glob("data/categories/*.yml").map { sys.parse_yaml(_1) } - @raw_integrations_data = sys.parse_yaml("data/integrations/integrations.yml") - mapping_rule_files = sys.glob("data/integrations/*/*/mappings/*_shopify.yml") - @raw_mapping_rules_data = mapping_rule_files.map { { content: sys.parse_yaml(_1), file_name: _1 } } - - SeedLocalCommand.new(quiet: true).run - end - - def after_all - Value.delete_all - Attribute.delete_all - Category.delete_all - AttributesValue.delete_all - CategoriesAttribute.delete_all - Integration.delete_all - MappingRule.delete_all - end - - test "AttributeValues are consistent with values.yml" do - @raw_values_data.each do |raw_value| - deserialized_value = Value.new_from_data(raw_value) - real_value = Value.find(raw_value.fetch("id")) - - assert_equal deserialized_value, real_value - end - end - - test "Attributes are consistent with attributes.yml" do - base_attributes = @raw_attributes_data["base_attributes"] - extended_attributes = @raw_attributes_data["extended_attributes"] - - base_attributes.each do |raw_attribute| - deserialized_attribute = Attribute.new_from_data(raw_attribute) - real_attribute = Attribute.find(raw_attribute.fetch("id")) - - assert_equal deserialized_attribute, real_attribute - end - - extended_attributes.each do |raw_attribute| - deserialized_attribute = Attribute.new_from_data(raw_attribute) - real_attribute = Attribute.find_by( - name: raw_attribute.fetch("name"), - base_friendly_id: raw_attribute.fetch("values_from"), - ) - - assert_equal deserialized_attribute.attributes.except("id"), real_attribute.attributes.except("id") - end - end - - test "Exteneded Attributes have primary attributes if they inherit values" do - @raw_attributes_data["extended_attributes"].each do |raw_attribute| - next unless raw_attribute.key?("values_from") - - real_attribute = Attribute.find_by( - name: raw_attribute.fetch("name"), - base_friendly_id: raw_attribute.fetch("values_from"), - ) - real_parent_attribute = Attribute.find_by!(friendly_id: raw_attribute.fetch("values_from")) - - assert_equal real_parent_attribute, real_attribute.base_attribute - end - end - - test "Categories are consistent with categories/*.yml" do - @raw_verticals_data.flatten.each do |raw_category| - deserialized_category = Category.new_from_data(raw_category) - real_category = Category.find(raw_category.fetch("id")) - - assert_equal deserialized_category, real_category - assert_equal raw_category.fetch("children").size, real_category.children.count - assert_equal deserialized_category.children, real_category.children - end - end - - test "Category ↔ Attribute relationships are consistent with categories/*.yml" do - @raw_verticals_data.flatten.each do |raw_category| - properties_via_raw_category_id = Category.find(raw_category.fetch("id")).related_attributes - properties_via_raw_attributes = raw_category.fetch("attributes").map { Attribute.find_by(friendly_id: _1) } - - assert_equal properties_via_raw_attributes.sort, properties_via_raw_category_id.sort - end - end - - test "Attribute ↔ Value relationships are consistent with attributes.yml base_attributes" do - @raw_attributes_data["base_attributes"].select { _1.key?("values") }.each do |raw_attribute| - values_via_raw_id = Attribute.find(raw_attribute.fetch("id")).values - values_via_raw_values = raw_attribute.fetch("values").map { Value.find_by(friendly_id: _1) } - - assert_equal values_via_raw_values.sort, values_via_raw_id.sort - end - end - - test "Attribute ↔ Value relationships are consistent with attributes.yml they are extended" do - @raw_attributes_data["extended_attributes"].select { _1.key?("values_from") }.each do |raw_attribute| - attribute_via_source = Attribute.find_by( - name: raw_attribute.fetch("name"), - base_friendly_id: raw_attribute.fetch("values_from"), - ) - attribute_via_values_from = Attribute.find_by(friendly_id: raw_attribute.fetch("values_from")) - - assert_equal attribute_via_values_from.values.sort, attribute_via_source.values.sort - end - end - - # more fragile, but easier sanity check - test "Snowboards category is fully imported and modeled correctly" do - snowboard = Category.find("sg-4-17-2-17") - - assert_equal "Snowboards", snowboard.name - assert_empty snowboard.children - - real_attribute_friendly_ids = snowboard.related_attributes.pluck(:friendly_id) - assert_equal 8, real_attribute_friendly_ids.size - assert_includes real_attribute_friendly_ids, "age_group" - assert_includes real_attribute_friendly_ids, "color" - assert_includes real_attribute_friendly_ids, "pattern" - assert_includes real_attribute_friendly_ids, "recommended_skill_level" - assert_includes real_attribute_friendly_ids, "snowboard_design" - assert_includes real_attribute_friendly_ids, "snowboarding_style" - assert_includes real_attribute_friendly_ids, "target_gender" - assert_includes real_attribute_friendly_ids, "snowboard_construction" - end - - # more fragile, but easier sanity check - test "Snowboard construction attribute <2894> is fully imported and modeled correctly" do - snowboard_construction = Attribute.find(2894) - - assert_equal "Snowboard construction", snowboard_construction.name - assert_equal "snowboard_construction", snowboard_construction.friendly_id - - real_value_friendly_ids = snowboard_construction.values.pluck(:friendly_id) - assert_equal 5, real_value_friendly_ids.size - assert_includes real_value_friendly_ids, "snowboard_construction__camber" - assert_includes real_value_friendly_ids, "snowboard_construction__flat" - assert_includes real_value_friendly_ids, "snowboard_construction__hybrid" - assert_includes real_value_friendly_ids, "snowboard_construction__rocker" - end - - test "MappingRule ↔ Product relationships are consistent with integrations/*/*/mappings/*_shopify.yml" do - @raw_mapping_rules_data.each do |raw| - from_shopify = File.basename(raw[:file_name], ".*").split("_")[0] == "from" - integration_name = Pathname.new(raw[:file_name]).each_filename.to_a[-4] - input_type = "ShopifyProduct" - output_type = "#{integration_name.capitalize}Product" - unless from_shopify - input_type, output_type = output_type, input_type - end - raw[:content].fetch("rules").each do |raw_rule| - input_id = Product.find_from_data(raw_rule["input"], type: input_type).id - output_id = Product.find_from_data(raw_rule["output"], type: output_type).id - - assert_predicate MappingRule.find_by(input_id:, output_id:), :present? - end - end - end -end diff --git a/test/integration/distribution_matches_data_test.rb b/test/integration/distribution_matches_data_test.rb deleted file mode 100644 index c05f4964d..000000000 --- a/test/integration/distribution_matches_data_test.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -require_relative "../test_helper" - -class DistributionMatchesDataTest < ActiveSupport::TestCase - include Minitest::Hooks - parallelize(workers: 1) # disable parallelization - - def setup - @sys = System.new - end - - test "dist/ files match the system" do - dist_files_before = @sys.glob("dist/en/**/*.{json,txt}").map { [_1, @sys.read_file(_1)] }.to_h - - SeedLocalCommand.new(interactive: false).execute - GenerateDistCommand.new(interactive: false).execute - - dist_files_after = @sys.glob("dist/en/**/*.{json,txt}").map { [_1, @sys.read_file(_1)] }.to_h - - files_added = dist_files_after.keys - dist_files_before.keys - files_removed = dist_files_before.keys - dist_files_after.keys - files_changed = dist_files_after.select { |k, v| dist_files_before[k] != v }.keys - - assert_empty(files_added, <<~MSG) - Expected, but did not find, these files: #{files_added.join("\n")}. - - If run locally, this test itself has fixed the issue. - MSG - assert_empty(files_removed, <<~MSG) - Found, but did not expect, these files: - #{files_removed.join("\n")} - - If run locally, this test itself has fixed the issue - MSG - assert_empty(files_changed, <<~MSG) - Expected changes to these files: - #{files_changed.join("\n")} - - If run locally, this test itself has fixed the issue - MSG - end -end diff --git a/test/integration/mapping_validation_test.rb b/test/integration/mapping_validation_test.rb deleted file mode 100644 index 90d6036a6..000000000 --- a/test/integration/mapping_validation_test.rb +++ /dev/null @@ -1,240 +0,0 @@ -# frozen_string_literal: true - -require_relative "../test_helper" - -class MappingValidationTest < ActiveSupport::TestCase - def setup - @sys = System.new - @mappings_json_data = @sys.parse_json("dist/en/integrations/all_mappings.json") - end - - test "category IDs in mappings are valid" do - invalid_categories = [] - raw_mappings_list = [] - mapping_rule_files = @sys.glob("data/integrations/*/*/mappings/*_shopify.yml") - mapping_rule_files.each do |file| - raw_mappings_list << @sys.parse_yaml(file) - end - - raw_mappings_list.each do |mapping| - ["input", "output"].each do |input_or_output| - input_or_output_taxonomy = if input_or_output == "input" - mapping["input_taxonomy"] - else - mapping["output_taxonomy"] - end - - invalid_category_ids = validate_mapping_category_ids( - mapping["rules"], - input_or_output, - input_or_output_taxonomy, - ) - next if invalid_category_ids.empty? - - invalid_categories << { - input_taxonomy: mapping["input_taxonomy"], - output_taxonomy: mapping["output_taxonomy"], - rules_input_or_output: input_or_output, - invalid_category_ids: invalid_category_ids, - } - end - end - - unless invalid_categories.empty? - puts "Invalid category ids are found in mappings for the following integrations:" - invalid_categories.each_with_index do |item, index| - puts "" - puts "[#{index + 1}] #{item[:input_taxonomy]} to #{item[:output_taxonomy]} in the rules #{item[:rules_input_or_output]} (#{item[:invalid_category_ids].size} invalid ids)" - - item[:invalid_category_ids].each do |category_id| - puts " - #{category_id}" - end - end - assert(invalid_categories.empty?, "Invalid category ids are found in mappings.") - end - end - - test "every Shopify category has corresponding channel mappings" do - shopify_categories_lack_mappings = [] - @mappings_json_data["mappings"].each do |mapping| - next unless mapping["input_taxonomy"].include?("shopify") - - all_shopify_category_ids = category_ids_from_taxonomy(mapping["input_taxonomy"]) - next if all_shopify_category_ids.nil? - - unmapped_category_ids = unmapped_category_ids_for_mappings( - mapping["input_taxonomy"], - mapping["output_taxonomy"], - ) - - unmapped_category_ids = if !unmapped_category_ids.nil? && - all_shopify_category_ids.first.include?("gid://shopify/TaxonomyCategory/") - unmapped_category_ids.map { |id| "gid://shopify/TaxonomyCategory/#{id}" }.to_set - end - - shopify_category_ids_from_mappings_input = mapping["rules"] - .map { _1.dig("input", "category", "id") } - .to_set - - missing_category_ids = all_shopify_category_ids - shopify_category_ids_from_mappings_input - unless unmapped_category_ids.nil? - missing_category_ids -= unmapped_category_ids - end - - next if missing_category_ids.empty? - - shopify_categories_lack_mappings << { - input_taxonomy: mapping["input_taxonomy"], - output_taxonomy: mapping["output_taxonomy"], - missing_category_ids: missing_category_ids.map { |id| id.split("/").last }, - } - end - - unless shopify_categories_lack_mappings.empty? - puts "Shopify Categories are missing mappings for the following integrations:" - shopify_categories_lack_mappings.each_with_index do |mapping, index| - puts "" - puts "[#{index + 1}] #{mapping[:input_taxonomy]} to #{mapping[:output_taxonomy]} (#{mapping[:missing_category_ids].size} missing)" - mapping[:missing_category_ids].each do |category_id| - puts " - #{category_id}" - end - end - assert(shopify_categories_lack_mappings.empty?, "Shopify Categories are missing mappings.") - end - end - - test "category IDs cannot be presented in the rules input and unmapped_product_category_ids at the same time" do - overlapped_category_ids_in_mappings = [] - @mappings_json_data["mappings"].each do |mapping| - category_ids_from_mappings_input = mapping["rules"] - .map { _1.dig("input", "category", "id").split("/").last } - .to_set - - unmapped_category_ids = unmapped_category_ids_for_mappings( - mapping["input_taxonomy"], - mapping["output_taxonomy"], - ) - next if unmapped_category_ids.nil? - - overlapped_category_ids = category_ids_from_mappings_input & unmapped_category_ids.to_set - next if overlapped_category_ids.empty? - - overlapped_category_ids_in_mappings << { - input_taxonomy: mapping["input_taxonomy"], - output_taxonomy: mapping["output_taxonomy"], - overlapped_category_ids: overlapped_category_ids, - } - end - - unless overlapped_category_ids_in_mappings.empty? - puts "Category IDs cannot be presented in both rules input and unmapped_product_category_ids at the same time for the following integrations:" - overlapped_category_ids_in_mappings.each_with_index do |mapping, index| - puts "" - puts "[#{index + 1}] #{mapping[:input_taxonomy]} to #{mapping[:output_taxonomy]} (#{mapping[:overlapped_category_ids].size} overlapped)" - mapping[:overlapped_category_ids].each do |category_id| - puts " - #{category_id}" - end - end - assert( - overlapped_category_ids_in_mappings.empty?, - "Category IDs cannot be presented in both rules input and unmapped_product_category_ids at the same time for the following integrations.", - ) - end - end - - test "Shopify taxonomy version is in consistent between VERSION file and mappings in the /data folder" do - shopify_taxonomy_version_from_file = "shopify/" + @sys.read_file("VERSION").strip - allowed_shopify_legacy_source_taxonomies = ["shopify/2022-02", "shopify/2024-07", "shopify/2024-10"] - mapping_rule_files = @sys.glob("data/integrations/*/*/mappings/*_shopify.yml") - files_include_inconsistent_shopify_taxonomy_version = [] - mapping_rule_files.each do |file| - raw_mappings = @sys.parse_yaml(file) - input_taxonomy = raw_mappings["input_taxonomy"] - output_taxonomy = raw_mappings["output_taxonomy"] - next if input_taxonomy == shopify_taxonomy_version_from_file - - next if allowed_shopify_legacy_source_taxonomies.include?(input_taxonomy) && - output_taxonomy == shopify_taxonomy_version_from_file - - files_include_inconsistent_shopify_taxonomy_version << { - file_path: file, - taxonomy_version: shopify_taxonomy_version_from_file, - } - end - - unless files_include_inconsistent_shopify_taxonomy_version.empty? - puts "The Shopify taxonomy version should be #{shopify_taxonomy_version_from_file} based on the VERSION file" - puts "We detected inconsistent Shopify taxonomy versions in the following mapping files in the /data folder:" - files_include_inconsistent_shopify_taxonomy_version.each_with_index do |item| - puts "- mapping file #{item[:file_path]} has inconsistent Shopify taxonomy version #{item[:taxonomy_version]}" - end - assert( - files_include_inconsistent_shopify_taxonomy_version.empty?, - "Shopify taxonomy version is inconsistent between VERSION file and mappings in the /data folder.", - ) - end - end - - def validate_mapping_category_ids(mapping_rules, input_or_output, input_or_output_taxonomy) - category_ids = category_ids_from_taxonomy(input_or_output_taxonomy).map { _1.split("/").last } - return [] if category_ids.nil? - - invalid_category_ids = Set.new - - mapping_rules.each do |rule| - product_category_ids = rule[input_or_output]["product_category_id"] - product_category_ids = [product_category_ids] unless product_category_ids.is_a?(Array) - - product_category_ids.each do |product_category_id| - invalid_category_ids.add(product_category_id) unless category_ids.include?(product_category_id.to_s) - end - end - - invalid_category_ids - end - - def category_ids_from_taxonomy(input_or_output_taxonomy) - if input_or_output_taxonomy.include?("shopify") && !input_or_output_taxonomy.include?("shopify/2022-02") - categories_json_data = @sys.parse_json("dist/en/categories.json") - shopify_category_ids = Set.new - categories_json_data["verticals"].each do |vertical| - vertical["categories"].each do |category| - shopify_category_ids.add(category["id"]) - end - end - shopify_category_ids - else - channel_category_ids = Set.new - file_path = "data/integrations/#{input_or_output_taxonomy}/full_names.yml" - channel_taxonomy = @sys.parse_yaml(file_path) - channel_taxonomy.each do |entry| - channel_category_ids.add(entry["id"].to_s) - end - channel_category_ids - end - end - - def unmapped_category_ids_for_mappings(mappings_input_taxonomy, mappings_output_taxonomy) - integration_mapping_path = if mappings_input_taxonomy.include?("shopify") && - mappings_output_taxonomy.include?("shopify") - integration_version = "shopify/2022-02" - if mappings_input_taxonomy == "shopify/2022-02" - "#{integration_version}/mappings/to_shopify.yml" - else - "#{integration_version}/mappings/from_shopify.yml" - end - elsif mappings_input_taxonomy.include?("shopify") - integration_version = mappings_output_taxonomy - "#{integration_version}/mappings/from_shopify.yml" - else - integration_version = mappings_input_taxonomy - "#{integration_version}/mappings/to_shopify.yml" - end - - file_path = "data/integrations/#{integration_mapping_path}" - return unless File.exist?(file_path) - - mappings = @sys.parse_yaml(file_path) - mappings["unmapped_product_category_ids"] if mappings.key?("unmapped_product_category_ids") - end -end diff --git a/test/models/application_test.rb b/test/models/application_test.rb deleted file mode 100644 index 881939520..000000000 --- a/test/models/application_test.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -require_relative "../test_helper" - -class ApplicationTest < ActiveSupport::TestCase - test "Factories are valid" do - skip - FactoryBot.lint(traits: true) - end -end diff --git a/test/models/attribute_test.rb b/test/models/attribute_test.rb deleted file mode 100644 index 83416db39..000000000 --- a/test/models/attribute_test.rb +++ /dev/null @@ -1,349 +0,0 @@ -# frozen_string_literal: true - -require_relative "../test_helper" - -class AttributeTest < ActiveSupport::TestCase - def setup - Attribute.stubs(:localizations).returns({}) - Value.stubs(:localizations).returns({}) - end - - def teardown - Attribute.delete_all - Value.delete_all - end - - test "default ordering is alphabetical" do - material = create(:attribute, name: "Material") - size = create(:attribute, name: "size") - color = create(:attribute, name: "Color") - - assert_equal [color, material, size], Attribute.all.to_a - end - - test ".base returns base attributes" do - base = create(:attribute, name: "Base", base_attribute: nil) - create(:attribute, name: "Extended", base_attribute: base) - - assert_equal [base], Attribute.base - end - - test ".extended returns attributes based off others" do - base = create(:attribute, name: "Base", base_attribute: nil) - extended = create(:attribute, name: "Extended", base_attribute: base) - - assert_equal [extended], Attribute.extended - end - - test "#gid returns a global id" do - assert_equal "gid://shopify/TaxonomyAttribute/42", build(:attribute, id: 42).gid - end - - test "#gid returns base_attribute.gid when extended" do - base = build(:attribute, id: 1, name: "Base", base_attribute: nil) - extended = build(:attribute, id: 2, name: "Extended", base_attribute: base) - - refute_equal base.id, extended.id - assert_equal base.gid, extended.gid - end - - test "#base?" do - assert_predicate base_attribute, :base? - refute_predicate extended_attribute, :base? - end - - test "#extended?" do - refute_predicate base_attribute, :extended? - assert_predicate extended_attribute, :extended? - end - - test "#friendly_id must be unique" do - create(:attribute, friendly_id: "material") - another_material = build(:attribute, friendly_id: "material") - - refute_predicate another_material, :valid? - end - - test "#values must match base_attribute#values" do - value = build(:value) - base_attribute.values = [value] - extended_attribute.values = [value] - - assert_predicate base_attribute, :valid? - assert_predicate extended_attribute, :valid? - - extended_attribute.values = [] - - refute_predicate extended_attribute, :valid? - end - - test ".new_from_data creates a new attribute" do - base_attribute = Attribute.new_from_data( - "id" => 1, - "name" => "Color", - "friendly_id" => "color", - "handle" => "color", - ) - - assert_equal 1, base_attribute.id - assert_equal "Color", base_attribute.name - assert_equal "color", base_attribute.friendly_id - assert_equal "color", base_attribute.handle - assert_nil base_attribute.base_friendly_id - - extended_attribute = Attribute.new_from_data( - "name" => "Swatch Color", - "friendly_id" => "swatch_color", - "handle" => "swatch-color", - "values_from" => "color", - ) - - assert_nil extended_attribute.id - assert_equal "Swatch Color", extended_attribute.name - assert_equal "swatch_color", extended_attribute.friendly_id - assert_equal "swatch-color", extended_attribute.handle - assert_equal "color", extended_attribute.base_friendly_id - end - - test ".insert_all_from_data creates multiple categories" do - data = [ - { - "id" => 1, - "name" => "Color", - "friendly_id" => "color", - "handle" => "color", - "description" => "Description for Color", - }, - { - "id" => 2, - "name" => "Size", - "friendly_id" => "size", - "handle" => "size", - "description" => "Description for Size", - }, - ] - - assert_difference -> { Attribute.count }, 2 do - Attribute.insert_all_from_data(data) - end - end - - test ".as_json returns distribution json" do - red = build(:value, name: "Red", handle: "color-red") - color = create(:attribute, name: "Color", values: [red]) - create(:attribute, name: "Swatch Color", handle: "swatch-color", base_attribute: color, values: [red]) - - color.reload - - assert_equal( - { - "version" => 1, - "attributes" => [ - { - "id" => "gid://shopify/TaxonomyAttribute/#{color.id}", - "name" => "Color", - "handle" => "color", - "description" => "Description for Color", - "extended_attributes" => [ - { - "name" => "Swatch Color", - "handle" => "swatch-color", - }, - ], - "values" => [ - { - "id" => "gid://shopify/TaxonomyValue/#{red.id}", - "name" => "Red", - "handle" => "color-red", - }, - ], - }, - ], - }, - Attribute.as_json([color], version: 1), - ) - end - - test ".as_json returns distribution json with values sorted in predetermined custom order" do - small = build(:value, name: "Small (S)", handle: "size__small-s", friendly_id: "size__small_s", position: 0) - medium = build(:value, name: "Medium (M)", handle: "size__medium-m", friendly_id: "size__medium_m", position: 1) - large = build(:value, name: "Large (L)", handle: "size__large-l", friendly_id: "size__large_l", position: 2) - size = create(:attribute, name: "Size", values: [large, medium, small]) - - attributes_json = Attribute.as_json([size], version: 1) - - assert_equal ["Small (S)", "Medium (M)", "Large (L)"], - attributes_json.dig("attributes", 0, "values").map { _1["name"] } - end - - test ".as_json returns distribution json with values sorted in predetermined custom order for other locales" do - Value.unstub(:localizations) - - small = build(:value, name: "Small (S)", handle: "size__small-s", friendly_id: "size__small_s", position: 0) - medium = build(:value, name: "Medium (M)", handle: "size__medium-m", friendly_id: "size__medium_m", position: 1) - large = build(:value, name: "Large (L)", handle: "size__large-l", friendly_id: "size__large_l", position: 2) - size = create(:attribute, name: "Size", values: [large, medium, small]) - - attributes_json = Attribute.as_json([size], version: 1, locale: "fr") - - assert_equal ["Petite taille (S)", "Taille moyenne (M)", "Taille large (L)"], - attributes_json.dig("attributes", 0, "values").map { _1["name"] } - end - - test ".as_txt returns padded and version string representation" do - color = create(:attribute, name: "Color") - size = create(:attribute, name: "Size") - - ids = [color.id, size.id] - lpad = ids.map { _1.to_s.size }.max - color_id, size_id = ids.map { _1.to_s.ljust(lpad) } - - assert_equal <<~TXT.strip, Attribute.as_txt([color, size], version: 1) - # Shopify Product Taxonomy - Attributes: 1 - # Format: {GID} : {Attribute name} - - gid://shopify/TaxonomyAttribute/#{color_id} : Color - gid://shopify/TaxonomyAttribute/#{size_id} : Size - TXT - end - - test "#as_json_for_data returns data json" do - red = build(:value, name: "Red") - color = create(:attribute, name: "Color", values: [red]) - - assert_equal( - { - "id" => color.id, - "name" => "Color", - "handle" => "color", - "description" => "Description for Color", - "friendly_id" => "color", - "values" => [red.friendly_id], - }, - color.as_json_for_data, - ) - - swatch_color = create( - :attribute, - name: "Swatch Color", - handle: "swatch-color", - base_attribute: color, - values: [red], - ) - - assert_equal( - { - "name" => "Swatch Color", - "handle" => "swatch-color", - "description" => "Description for Swatch Color", - "friendly_id" => "swatch-color", - "values_from" => "color", - }, - swatch_color.as_json_for_data, - ) - end - - test "#as_json returns attributes' values sorted alphanumerically" do - red = build(:value, name: "Red", handle: "color__red", friendly_id: "color__red") - blue = build(:value, name: "Blue", handle: "color__blue", friendly_id: "color__blue") - green = build(:value, name: "Green", handle: "color__green", friendly_id: "color__green") - color = create(:attribute, name: "Color", values: [blue, green, red]) - - color_json = color.as_json["values"] - - assert_equal ["Blue", "Green", "Red"], color_json.map { _1["name"] } - end - - test "#as_json returns attributes' values sorted alphanumerically for all locales" do - Value.unstub(:localizations) - - red = build(:value, name: "Red", handle: "color__red", friendly_id: "color__red") - blue = build(:value, name: "Blue", handle: "color__blue", friendly_id: "color__blue") - green = build(:value, name: "Green", handle: "color__green", friendly_id: "color__green") - color = create(:attribute, name: "Color", values: [blue, green, red]) - - color_json = color.as_json(locale: "fr")["values"] - - assert_equal ["Bleu", "Rouge", "Vert"], color_json.map { _1["name"] } - end - - test "#as_json returns attributes' values with `other` listed last for all locales" do - Value.unstub(:localizations) - - animal = build(:value, name: "Animal", handle: "pattern__animal", friendly_id: "pattern__animal") - striped = build(:value, name: "Striped", handle: "pattern__striped", friendly_id: "pattern__striped") - other = build(:value, name: "Other", handle: "pattern__other", friendly_id: "pattern__other") - pattern = create(:attribute, name: "Pattern", values: [striped, animal, other]) - - pattern_json = pattern.as_json(locale: "fr")["values"] - - assert_equal ["Animal", "RayΓ©", "Autre"], pattern_json.map { _1["name"] } - end - - test "raises error when value names are provided with base attribute" do - base_attribute = create(:attribute) - assert_raises(RuntimeError, "Value names are not allowed when extending a base attribute") do - Attribute.find_or_create!( - "Test Attribute", - "Description", - base_attribute: base_attribute, - value_names: ["Value1"], - ) - end - end - - test "raises error when value names are missing for base attribute creation" do - assert_raises(RuntimeError, "Value names are required when creating a base attribute") do - Attribute.find_or_create!("Test Attribute", "Description", value_names: []) - end - end - - test "raises error when attribute already exists" do - create(:attribute, name: "Material") - assert_raises(RuntimeError, "Attribute already exists") do - Attribute.find_or_create!("Material", "Description", value_names: ["Value1"]) - end - end - - test "creates a new base attribute with values" do - assert_difference "Attribute.count", 1 do - assert_difference "Value.count", 2 do - Attribute.find_or_create!("Test Attribute", "Description", value_names: ["Value1", "Value2"]) - end - end - attribute = Attribute.find_by(friendly_id: "test_attribute") - assert_not_nil attribute - assert_equal ["Value1", "Value2"], attribute.values.map(&:name) - end - - test "creates a new extended attribute from base attribute" do - base_attribute = Attribute.new_from_data( - "id" => 1, - "name" => "Color", - "friendly_id" => "color", - "handle" => "color", - ) - - Value.find_or_create_for_attribute!(base_attribute, "Blue") - Value.find_or_create_for_attribute!(base_attribute, "Green") - - assert_difference "Attribute.count", 1 do - assert_no_difference "Value.count" do - Attribute.find_or_create!("Extended Attribute", "Description", base_attribute: base_attribute) - end - end - attribute = Attribute.find_by(friendly_id: "extended_attribute") - assert_not_nil attribute - assert_equal ["Blue", "Green"], attribute.values.map(&:name) - end - - private - - def base_attribute - @base_attribute ||= build(:attribute) - end - - def extended_attribute - @extended_attribute ||= build(:attribute, base_attribute:) - end -end diff --git a/test/models/category_test.rb b/test/models/category_test.rb deleted file mode 100644 index 5b06e64e0..000000000 --- a/test/models/category_test.rb +++ /dev/null @@ -1,304 +0,0 @@ -# frozen_string_literal: true - -require_relative "../test_helper" - -class CategoryTest < ActiveSupport::TestCase - def setup - Category.stubs(:localizations).returns({}) - Attribute.stubs(:localizations).returns({}) - end - - def teardown - Category.delete_all - Attribute.delete_all - end - - test ".gid returns a global id" do - assert_equal "gid://shopify/TaxonomyCategory/aa", Category.gid("aa") - end - - test "#id must follow parent's id" do - assert_predicate build(:category, id: "aa-0", parent:), :valid? - assert_predicate build(:category, id: "aa-123232", parent:), :valid? - assert_predicate build(:category, id: "bb-0", parent:), :invalid? - - child = build(:category, id: "aa-0", parent:) - assert_predicate build(:category, id: "aa-0-1", parent: child), :valid? - assert_predicate build(:category, id: "aa-1-1", parent: child), :invalid? - end - - test "#id for root must be only 2 chars" do - assert_predicate build(:category, id: "tt"), :valid? - - assert_predicate build(:category, id: "t"), :invalid? - assert_predicate build(:category, id: "ttt"), :invalid? - assert_predicate build(:category, id: "01"), :invalid? - end - - test "#id must match depth" do - assert_predicate build(:category, id: "aa-t"), :invalid? - assert_predicate build(:category, id: "aa-0-1", parent:), :invalid? - - assert_predicate build(:category, id: "aa-0", parent:), :valid? - end - - test "#gid returns a global id" do - assert_equal "gid://shopify/TaxonomyCategory/aa", parent.gid - assert_equal "gid://shopify/TaxonomyCategory/aa-42", build(:category, id: "aa-42").gid - end - - test "#next_child_id returns the next child id" do - child.save! - parent.reload - - assert_equal "aa-1", child.id - assert_equal "aa-2", parent.next_child_id - end - - test "#next_child_id returns the next child id for children" do - grandchild.save! - child.reload - - assert_equal "aa-1-1", grandchild.id - assert_equal "aa-1-2", child.next_child_id - end - - test "#friendly_name handleizes just right" do - assert_equal "aa_category", build(:category, id: "aa", name: "Category").friendly_name - assert_equal "aa-12_child", build(:category, id: "aa-12", name: "Child", parent:).friendly_name - # some real examples - assert_equal "aa_apparel_accessories", build(:category, id: "aa", name: "Apparel & Accessories").friendly_name - assert_equal "fb_food_beverages_tobacco", - build(:category, id: "fb", name: "Food, Beverages & Tobacco").friendly_name - end - - test "#root returns the top-most category node" do - assert_equal parent, child.root - assert_equal parent, grandchild.root - end - - test "#ancestors walk up the tree" do - assert_equal [child, parent], grandchild.ancestors - end - - test "#ancestors_and_self includes self" do - assert_equal [grandchild, child, parent], grandchild.ancestors_and_self - end - - test "#ascendant_of? checks if a category is a descendant" do - grandchild.save! - parent.reload - - assert parent.ancestor_of?(child) - assert parent.ancestor_of?(grandchild) - - refute child.ancestor_of?(parent) - refute grandchild.ancestor_of?(parent) - end - - test "#children are sorted by name" do - beta_child = create(:category, name: "Beta", parent:) - alpha_child = create(:category, name: "Alpha", parent:) - parent.reload - - assert_equal [alpha_child, beta_child], parent.children.to_a - end - - test "#descendants is depth-first" do - l2_beta = create(:category, name: "Beta", parent: child) - l2_alpha = create(:category, name: "Alpha", parent: child) - l3_child = create(:category, parent: l2_alpha) - parent.reload - - assert_equal [child, l2_alpha, l3_child, l2_beta], parent.descendants - end - - test "#descendants_and_self includes self" do - grandchild.save! - parent.reload - - assert_equal [parent, child, grandchild], parent.descendants_and_self - end - - test "#descendant_of? checks if a category is an ancestor" do - assert child.descendant_of?(parent) - assert grandchild.descendant_of?(parent) - - refute parent.descendant_of?(child) - refute parent.descendant_of?(grandchild) - end - - test "#reparent_to! reparents category and its descendents" do - grandchild.save! - - child.reparent_to!(create(:category, id: "bb")) - child.reload - - assert_equal "bb-1", child.id - assert_equal "bb-1-1", child.children.first.id - end - - test "#reparent_to! reparents with manually set new id" do - grandchild.save! - - child.reparent_to!(create(:category, id: "bb"), new_id: "bb-42") - child.reload - - assert_predicate Category.find("bb"), :valid? - assert_equal "bb-42", child.id - assert_equal "bb-42-1", child.children.first.id - end - - test "#reparent_to! updates all relationships across descendants" do - color = build(:attribute, name: "Color") - size = build(:attribute, name: "Size") - child.related_attributes = [color] - grandchild.related_attributes = [color, size] - grandchild.save! - - child.reparent_to!(create(:category, id: "bb")) - child.reload - - assert_empty parent.children - assert_equal Category.find("bb"), child.parent - assert_equal child, child.children.first.parent - assert_equal [color], child.related_attributes - assert_equal [color, size].sort, child.children.first.related_attributes.sort - end - - test "#reparent_to! ensures sensible targets" do - other_parent = create(:category, id: "bb") - create(:category, id: "bb-1", parent: other_parent) - - vertical_error = assert_raises(Category::ReparentError) { parent.reparent_to!(other_parent) } - descendant_error = assert_raises(Category::ReparentError) { child.reparent_to!(grandchild) } - invalid_id_error = assert_raises(Category::ReparentError) { child.reparent_to!(other_parent, new_id: "cc-1") } - id_taken_error = assert_raises(Category::ReparentError) { child.reparent_to!(other_parent, new_id: "bb-1") } - - assert_match(/vertical/, vertical_error.message) - assert_match(/descendant/, descendant_error.message) - assert_match(/new_id .* is invalid for parent/, invalid_id_error.message) - assert_match(/new_id .* is already taken/, id_taken_error.message) - end - - test ".new_from_data creates a new category" do - category = Category.new_from_data("id" => "aa", "name" => "Alpha") - - assert_equal "aa", category.id - assert_equal "Alpha", category.name - assert_nil category.parent_id - - child_category = Category.new_from_data("id" => "aa-0", "name" => "Beta") - - assert_equal "aa-0", child_category.id - assert_equal "Beta", child_category.name - assert_equal "aa", child_category.parent_id - end - - test ".insert_all_from_data creates multiple categories" do - data = [ - { "id" => "aa", "name" => "Alpha" }, - { "id" => "aa-0", "name" => "Beta" }, - ] - - assert_difference -> { Category.count }, 2 do - Category.insert_all_from_data(data) - end - end - - test ".as_json returns distribution json" do - child.save! - parent.reload - - assert_equal( - { - "version" => 1, - "verticals" => [ - { - "name" => "Category aa", - "prefix" => "aa", - "categories" => [ - { - "id" => "gid://shopify/TaxonomyCategory/aa", - "level" => 0, - "name" => "Category aa", - "full_name" => "Category aa", - "parent_id" => nil, - "attributes" => [], - "children" => [ - { - "id" => "gid://shopify/TaxonomyCategory/aa-1", - "name" => "Category aa-1", - }, - ], - "ancestors" => [], - }, - { - "id" => "gid://shopify/TaxonomyCategory/aa-1", - "level" => 1, - "name" => "Category aa-1", - "full_name" => "Category aa > Category aa-1", - "parent_id" => "gid://shopify/TaxonomyCategory/aa", - "attributes" => [], - "children" => [], - "ancestors" => [ - { - "id" => "gid://shopify/TaxonomyCategory/aa", - "name" => "Category aa", - }, - ], - }, - ], - }, - ], - }, - Category.as_json([parent], version: 1), - ) - end - - test ".as_txt returns padded and version string representation" do - child.save! - parent.reload - - assert_equal <<~TXT.strip, Category.as_txt([parent], version: 1) - # Shopify Product Taxonomy - Categories: 1 - # Format: {GID} : {Ancestor name} > ... > {Category name} - - gid://shopify/TaxonomyCategory/aa : Category aa - gid://shopify/TaxonomyCategory/aa-1 : Category aa > Category aa-1 - TXT - end - - test "#as_json_for_data returns data json" do - child.save! - color = create(:attribute, name: "Color") - parent.related_attributes = [color] - parent.save! - - parent.reload - - assert_equal( - { - "id" => "aa", - "name" => "Category aa", - "children" => ["aa-1"], - "attributes" => [color.friendly_id], - }, - parent.as_json_for_data, - ) - end - - private - - def parent - @parent ||= build(:category, id: "aa") - end - - def child - @child ||= build(:category, id: "aa-1", parent:) - end - - def grandchild - @grandchild ||= build(:category, id: "aa-1-1", parent: child) - end -end diff --git a/test/models/mapping_rule_test.rb b/test/models/mapping_rule_test.rb deleted file mode 100644 index 93b9d49ed..000000000 --- a/test/models/mapping_rule_test.rb +++ /dev/null @@ -1,240 +0,0 @@ -# frozen_string_literal: true - -require_relative "../test_helper" - -class MappingRuleTest < ActiveSupport::TestCase - def teardown - Category.delete_all - Product.delete_all - GoogleProduct.delete_all - MappingRule.delete_all - Integration.delete_all - Attribute.delete_all - Value.delete_all - end - - test ".as_json returns distribution json" do - integration_shopify.save! - mapping_rule.save! - category.save! - - assert_equal( - { - "version" => 1, - "mappings" => [ - { - "input_taxonomy" => "shopify/2022-02", - "output_taxonomy" => "google/2021-09-21", - "rules" => [ - { - "input" => { - "category" => { - "id" => "gid://shopify/TaxonomyCategory/aa", - "full_name" => "Apparel & Accessories", - }, - }, - "output" => { - "category" => [ - { - "id" => "166", - "full_name" => "Apparel & Accessories", - }, - ], - }, - }, - ], - }, - ], - }, - MappingRule.as_json([mapping_rule], version: 1), - ) - end - - test "#as_json returns data json" do - category.save! - assert_equal( - { - "input" => { - "category" => { - "id" => "gid://shopify/TaxonomyCategory/aa", - "full_name" => "Apparel & Accessories", - }, - }, - "output" => { - "category" => [ - { - "id" => "166", - "full_name" => "Apparel & Accessories", - }, - ], - }, - }, - mapping_rule.as_json, - ) - end - - test "#as_json returns resolved attributes when present" do - category.save! - attribute.save! - value.save! - - assert_equal( - { - "input" => { - "category" => { - "id" => "gid://shopify/TaxonomyCategory/aa", - "full_name" => "Apparel & Accessories", - }, - "attributes" => [{ "attribute" => attribute.gid, "value" => value.gid }], - }, - "output" => { - "category" => [ - { - "id" => "166", - "full_name" => "Apparel & Accessories", - }, - ], - }, - }, - mapping_rule_with_attributes.as_json.sort.to_h, - ) - end - - test ".as_txt returns version string representation" do - assert_equal <<~TXT.strip, MappingRule.as_txt([mapping_rule], version: 1) - # Shopify Product Taxonomy - Mapping shopify/2022-02 to google/2021-09-21 - # Format: - # β†’ {base taxonomy category name} - # β‡’ {mapped taxonomy category name} - - β†’ Apparel & Accessories - β‡’ Apparel & Accessories - TXT - end - - test ".as_txt generates a version string and omits entries where the input and output categories originate from the same taxonomy and have identical names" do - mapping_same = build( - :mapping_rule, - integration_id: integration_shopify.id, - input: build( - :product, - payload: { "properties" => nil, "product_category_id" => "gid://shopify/TaxonomyCategory/1" }, - full_name: "Apparel & Accessories", - ), - output: shopify_product, - input_version: "shopify/v0", - output_version: "shopify/v1", - ) - mapping_short = build( - :mapping_rule, - integration_id: integration_shopify.id, - input: build( - :product, - payload: { "properties" => nil, "product_category_id" => "gid://shopify/TaxonomyCategory/100001" }, - full_name: "Media > DVDs & Videos", - ), - output: build( - :product, - payload: { "properties" => nil, "product_category_id" => "gid://shopify/TaxonomyCategory/me" }, - full_name: "Media > Videos", - ), - input_version: "shopify/v0", - output_version: "shopify/v1", - ) - mapping_long = build( - :mapping_rule, - integration_id: integration_shopify.id, - input: build( - :product, - payload: { "properties" => nil, "product_category_id" => "gid://shopify/TaxonomyCategory/200002" }, - full_name: "Electronics > Communications > Telephony > Mobile Phone Accessories > Mobile Phone Cases", - ), - output: build( - :product, - payload: { "properties" => nil, "product_category_id" => "gid://shopify/TaxonomyCategory/el-4-8-4" }, - full_name: "Electronics > Communications > Telephony > Mobile & Smart Phone Accessories > Mobile Phone Cases", - ), - input_version: "shopify/v0", - output_version: "shopify/v1", - ) - - assert_equal <<~TXT.strip, MappingRule.as_txt([mapping_same, mapping_short, mapping_long], version: 1) - # Shopify Product Taxonomy - Mapping shopify/v0 to shopify/v1 - # Format: - # β†’ {base taxonomy category name} - # β‡’ {mapped taxonomy category name} - - β†’ Electronics > Communications > Telephony > Mobile Phone Accessories > Mobile Phone Cases - β‡’ Electronics > Communications > Telephony > Mobile & Smart Phone Accessories > Mobile Phone Cases - - β†’ Media > DVDs & Videos - β‡’ Media > Videos - TXT - end - - private - - def integration_shopify - @integration_shopify ||= build(:integration, name: "shopify", available_versions: ["shopify/v1"]) - end - - def mapping_rule - @mapping_rule ||= build( - :mapping_rule, - integration_id: integration_shopify.id, - input: shopify_product, - output: google_product, - input_version: "shopify/2022-02", - output_version: "google/2021-09-21", - ) - end - - def mapping_rule_with_attributes - @mapping_rule_with_attributes ||= build( - :mapping_rule, - integration_id: integration_shopify.id, - input: shopify_product_with_attributes, - output: google_product, - input_version: "shopify/2022-02", - output_version: "google/2021-09-21", - ) - end - - def shopify_product - @shopify_product ||= build( - :product, - payload: { "properties" => nil, "product_category_id" => "gid://shopify/TaxonomyCategory/aa" }, - full_name: "Apparel & Accessories", - ) - end - - def shopify_product_with_attributes - @shopify_product_with_attributes ||= build( - :product, - payload: { - "properties" => [{ "attribute" => attribute.friendly_id, "value" => value.friendly_id }], - "product_category_id" => "gid://shopify/TaxonomyCategory/aa", - }, - ) - end - - def google_product - @google_product ||= build( - :google_product, - payload: { "properties" => nil, "product_category_id" => ["166"] }, - full_name: "Apparel & Accessories", - ) - end - - def attribute - @attribute ||= build(:attribute) - end - - def category - @category ||= build(:category, id: "aa", name: "Apparel & Accessories") - end - - def value - @value ||= build(:value) - end -end diff --git a/test/models/value_sorter_test.rb b/test/models/value_sorter_test.rb deleted file mode 100644 index 949879c4b..000000000 --- a/test/models/value_sorter_test.rb +++ /dev/null @@ -1,85 +0,0 @@ -# frozen_string_literal: true - -require_relative "../test_helper" - -class ValueSorterTest < ActiveSupport::TestCase - def teardown - Attribute.delete_all - Value.delete_all - end - - test ".sort sorts values by position if present" do - size = construct_size_attribute - - sorted_values = ValueSorter.sort(size.values) - - assert_equal ["Small (S)", "Medium (M)", "Large (L)"], sorted_values.map(&:name) - end - - test ".sort sorts non-English values by position if present" do - size = construct_size_attribute - - sorted_values = ValueSorter.sort(size.values, locale: "fr") - - assert_equal ["Petite taille (S)", "Taille moyenne (M)", "Taille large (L)"], - sorted_values.map { _1.name(locale: "fr") } - end - - test ".sort sorts values alphanumerically" do - color = construct_color_attribute - - sorted_values = ValueSorter.sort(color.values) - - assert_equal ["Blue", "Green", "Red"], sorted_values.map(&:name) - end - - test ".sort sorts non-English values alphanumerically" do - color = construct_color_attribute - - sorted_values = ValueSorter.sort(color.values, locale: "fr") - - assert_equal ["Bleu", "Rouge", "Vert"], sorted_values.map { _1.name(locale: "fr") } - end - - test ".sort sorts values with 'Other' at the end" do - pattern = construct_pattern_attribute - - sorted_values = ValueSorter.sort(pattern.values) - - assert_equal ["Animal", "Striped", "Other"], sorted_values.map(&:name) - end - - test ".sort sorts non-English values with 'Other' at the end" do - pattern = construct_pattern_attribute - - sorted_values = ValueSorter.sort(pattern.values, locale: "fr") - - assert_equal ["Animal", "RayΓ©", "Autre"], sorted_values.map { _1.name(locale: "fr") } - end - - private - - def construct_color_attribute - red = build(:value, name: "Red", handle: "color__red", friendly_id: "color__red") - blue = build(:value, name: "Blue", handle: "color__blue", friendly_id: "color__blue") - green = build(:value, name: "Green", handle: "color__green", friendly_id: "color__green") - - create(:attribute, name: "Color", handle: "color", values: [red, blue, green]) - end - - def construct_pattern_attribute - animal = build(:value, name: "Animal", handle: "pattern__animal", friendly_id: "pattern__animal") - striped = build(:value, name: "Striped", handle: "pattern__striped", friendly_id: "pattern__striped") - other = build(:value, name: "Other", handle: "pattern__other", friendly_id: "pattern__other") - - create(:attribute, name: "Pattern", values: [striped, animal, other]) - end - - def construct_size_attribute - small = build(:value, name: "Small (S)", handle: "size__small-s", friendly_id: "size__small_s", position: 0) - medium = build(:value, name: "Medium (M)", handle: "size__medium-m", friendly_id: "size__medium_m", position: 1) - large = build(:value, name: "Large (L)", handle: "size__large-l", friendly_id: "size__large_l", position: 2) - - create(:attribute, name: "Size", values: [medium, large, small]) - end -end diff --git a/test/models/value_test.rb b/test/models/value_test.rb deleted file mode 100644 index 27a3f82fe..000000000 --- a/test/models/value_test.rb +++ /dev/null @@ -1,189 +0,0 @@ -# frozen_string_literal: true - -require_relative "../test_helper" - -class ValueTest < ActiveSupport::TestCase - def setup - Attribute.stubs(:localizations).returns({}) - Value.stubs(:localizations).returns({}) - end - - def teardown - Attribute.delete_all - Value.delete_all - end - - test "default ordering is alphabetical with 'Other' last" do - other = create(:value, name: "Other") - zoo = create(:value, name: "Zoo") - red = create(:value, name: "Red") - - assert_equal [red, zoo, other], Value.all.to_a - end - - test "#gid returns a global id" do - assert_equal "gid://shopify/TaxonomyValue/42", build(:value, id: 42).gid - end - - test "#full_name returns the name of the primary attribute and the value" do - assert_equal "Gold [Color]", build(:value, name: "Gold", primary_attribute: color_attribute).full_name - end - - test "#friendly_id must be unique" do - create(:value, friendly_id: "wonderful-gold") - another_gold = build(:value, friendly_id: "wonderful-gold") - - refute_predicate another_gold, :valid? - end - - test "#handle must be unique per primary attribute" do - create(:value, handle: "gold", primary_attribute: color_attribute) - another_gold = build(:value, handle: "gold", primary_attribute: color_attribute) - - refute_predicate another_gold, :valid? - end - - test "#handle can be duplicated across different primary attributes" do - create(:value, handle: "gold", primary_attribute: color_attribute) - material_gold = build(:value, handle: "gold", primary_attribute: build(:attribute, name: "Material")) - - assert_predicate material_gold, :valid? - end - - test ".new_from_data creates a new attribute value" do - color_attribute.save! - attribute_value = Value.new_from_data( - "id" => 1, - "name" => "Gold", - "handle" => "gold", - "friendly_id" => "color__gold", - ) - - assert_equal 1, attribute_value.id - assert_equal "Gold", attribute_value.name - assert_equal "gold", attribute_value.handle - assert_equal "color__gold", attribute_value.friendly_id - assert_equal "color", attribute_value.primary_attribute_friendly_id - end - - test ".insert_all_from_data creates multiple values" do - data = [ - { - "id" => 1, - "name" => "Gold", - "handle" => "gold", - "friendly_id" => "color__gold", - "primary_attribute_friendly_id" => "color", - }, - { - "id" => 2, - "name" => "Red", - "handle" => "red", - "friendly_id" => "color__red", - "primary_attribute_friendly_id" => "color", - }, - ] - - base_attributes = [{ "id" => 1, "name" => "Color", "friendly_id" => "color" }] - - assert_difference -> { Value.count }, 2 do - Value.insert_all_from_data(data, base_attributes) - - assert_nil Value.find_by(friendly_id: "color__gold").position - assert_nil Value.find_by(friendly_id: "color__red").position - end - end - - test ".insert_all_from_data assigns positions to pre-sorted values" do - data = [ - { - "id" => 1, - "name" => "Small (S)", - "handle" => "size__small-s", - "friendly_id" => "size__small_s", - "primary_attribute_friendly_id" => "size", - }, - { - "id" => 2, - "name" => "Medium (M)", - "handle" => "size__medium-m", - "friendly_id" => "size__medium_m", - "primary_attribute_friendly_id" => "size", - }, - ] - - base_attributes = [ - { - "id" => 1, - "name" => "Size", - "friendly_id" => "size", - "sorting" => "custom", - "values" => ["size__small_s", "size__medium_m"], - }, - ] - - assert_difference -> { Value.count }, 2 do - Value.insert_all_from_data(data, base_attributes) - - assert_equal 0, Value.find_by(friendly_id: "size__small_s").position - assert_equal 1, Value.find_by(friendly_id: "size__medium_m").position - end - end - - test ".as_json returns distribution json" do - gold = create(:value, name: "Gold", handle: "color-gold") - red = create(:value, name: "Red", handle: "color-red") - - assert_equal( - { - "version" => 1, - "values" => [ - { - "id" => "gid://shopify/TaxonomyValue/#{gold.id}", - "name" => "Gold", - "handle" => "color-gold", - }, - { - "id" => "gid://shopify/TaxonomyValue/#{red.id}", - "name" => "Red", - "handle" => "color-red", - }, - ], - }, - Value.as_json([gold, red], version: 1), - ) - end - - test ".as_txt returns padded and version string representation" do - gold = create(:value, name: "Gold", primary_attribute: color_attribute) - red = create(:value, name: "Red", primary_attribute: color_attribute) - - assert_equal <<~TXT.strip, Value.as_txt([gold, red], version: 1) - # Shopify Product Taxonomy - Attribute Values: 1 - # Format: {GID} : {Value name} [{Attribute name}] - - gid://shopify/TaxonomyValue/#{gold.id} : Gold [Color] - gid://shopify/TaxonomyValue/#{red.id} : Red [Color] - TXT - end - - test "#as_json_for_data returns data json" do - gold = create(:value, name: "Gold", primary_attribute: color_attribute) - - assert_equal( - { - "id" => gold.id, - "name" => "Gold", - "friendly_id" => "color__gold", - "handle" => "color-gold", - }, - gold.as_json_for_data, - ) - end - - private - - def color_attribute - @color_attribute ||= build(:attribute, name: "Color") - end -end diff --git a/test/test_helper.rb b/test/test_helper.rb deleted file mode 100644 index 740601023..000000000 --- a/test/test_helper.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -ENV["RAILS_ENV"] ||= "test" -require_relative "../config/environment" -require "minitest/pride" -require "rails/test_help" -require "mocha/minitest" - -module ActiveSupport - class TestCase - include FactoryBot::Syntax::Methods - - parallelize workers: :number_of_processors - fixtures :all - - self.use_transactional_tests = true - end -end diff --git a/tmp/.gitkeep b/tmp/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/vendor/.gitkeep b/vendor/.gitkeep deleted file mode 100644 index e69de29bb..000000000