Skip to content

Commit

Permalink
Adds option value decoder utility functions (#2425)
Browse files Browse the repository at this point in the history
Co-authored-by: Aaron Richner <[email protected]>
Co-authored-by: Helen Lin <[email protected]>
  • Loading branch information
3 people authored Oct 21, 2024
1 parent 8337e53 commit 76cd4f9
Show file tree
Hide file tree
Showing 11 changed files with 549 additions and 1 deletion.
6 changes: 6 additions & 0 deletions .changeset/wet-garlics-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@shopify/hydrogen': patch
'@shopify/hydrogen-react': patch
---

Add utility functions `decodeEncodedVariant` and `isOptionValueCombinationInEncodedVariant` for parsing `product.encodedVariantExistence` and `product.encodedVariantAvailability` fields.
4 changes: 4 additions & 0 deletions packages/hydrogen-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export {useLoadScript} from './load-script.js';
export {MediaFile} from './MediaFile.js';
export {ModelViewer} from './ModelViewer.js';
export {Money} from './Money.js';
export {
decodeEncodedVariant,
isOptionValueCombinationInEncodedVariant,
} from './optionValueDecoder.js';
export {type ParsedMetafields, parseMetafield} from './parse-metafield.js';
export {ProductPrice} from './ProductPrice.js';
export {ProductProvider, useProduct} from './ProductProvider.js';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs';

const data: ReferenceEntityTemplateSchema = {
name: 'decodeEncodedVariant',
category: 'utilities',
isVisualComponent: false,
related: [
{
name: 'isOptionValueCombinationInEncodedVariant',
type: 'utility',
url: '/docs/api/hydrogen/2024-10/utilities/isOptionValueCombinationInEncodedVariant',
},
],
description:
'Decodes an encoded option value string into an array of option value combinations.',
type: 'utility',
defaultExample: {
description: 'Decode an encoded option value string',
codeblock: {
tabs: [
{
title: 'JavaScript',
code: './optionValueDecoder.decodeEncodedVariant.example.js',
language: 'js',
},
],
title: 'Example code',
},
},
definitions: [
{
title: 'Props',
type: 'DecodeEncodedVariantGeneratedType',
description: '',
},
],
};

export default data;
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {decodeEncodedVariant} from '@shopify/hydrogen-react';

// product.options = [
// {
// name: 'Color',
// optionValues: [
// {name: 'Red'},
// {name: 'Blue'},
// {name: 'Green'},
// ]
// },
// {
// name: 'Size',
// optionValues: [
// {name: 'S'},
// {name: 'M'},
// {name: 'L'},
// ]
// }
// ]

const encodedVariantAvailability = 'v1_0:0-2,1:2,';

const decodedVariantAvailability = decodeEncodedVariant(
encodedVariantAvailability,
);

// decodedVariantAvailability
// {
// [0,0], // Red, S
// [0,1], // Red, M
// [0,2], // Red, L
// [1,2] // Blue, L
// }
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs';

const data: ReferenceEntityTemplateSchema = {
name: 'isOptionValueCombinationInEncodedVariant',
category: 'utilities',
isVisualComponent: false,
related: [
{
name: 'decodeEncodedVariant',
type: 'utility',
url: '/docs/api/hydrogen/2024-10/utilities/decodeEncodedVariant',
},
],
description: `
Determines whether an option value combination is present in an encoded option value string.\n\n\`targetOptionValueCombination\` - Indices of option values to look up in the encoded option value string. A partial set of indices may be passed to determine whether a node or any children is present. For example, if a product has 3 options, passing \`[0]\` will return true if any option value combination for the first option's option value is present in the encoded string.
`,
type: 'utility',
defaultExample: {
description: 'Check if option value is in encoding',
codeblock: {
tabs: [
{
title: 'JavaScript',
code: './optionValueDecoder.isOptionValueCombinationInEncodedVariant.example.js',
language: 'js',
},
],
title: 'Example code',
},
},
definitions: [
{
title: 'Props',
type: 'IsOptionValueCombinationInEncodedVariantForDocs',
description: '',
},
],
};

export default data;
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {isOptionValueCombinationInEncodedVariant} from '@shopify/hydrogen-react';

// product.options = [
// {
// name: 'Color',
// optionValues: [
// {name: 'Red'},
// {name: 'Blue'},
// {name: 'Green'},
// ]
// },
// {
// name: 'Size',
// optionValues: [
// {name: 'S'},
// {name: 'M'},
// {name: 'L'},
// ]
// }
// ]
const encodedVariantExistence = 'v1_0:0-1,1:2,';

// For reference: decoded encodedVariantExistence
// {
// [0,0], // Red, S
// [0,1], // Red, M
// [1,2] // Blue, L
// }

// Returns true since there are variants exist for [Red]
isOptionValueCombinationInEncodedVariant([0], encodedVariantExistence); // true

isOptionValueCombinationInEncodedVariant([0, 0], encodedVariantExistence); // true
isOptionValueCombinationInEncodedVariant([0, 1], encodedVariantExistence); // true
isOptionValueCombinationInEncodedVariant([0, 2], encodedVariantExistence); // false - no variant exist for [Red, L]

// Returns true since there is a variant exist for [Blue]
isOptionValueCombinationInEncodedVariant([1], encodedVariantExistence); // true

isOptionValueCombinationInEncodedVariant([1, 0], encodedVariantExistence); // false - no variant exist for [Blue, S]
isOptionValueCombinationInEncodedVariant([1, 1], encodedVariantExistence); // false - no variant exist for [Blue, M]
isOptionValueCombinationInEncodedVariant([1, 2], encodedVariantExistence); // true

// Returns false since there is no variant exist for [Green]
isOptionValueCombinationInEncodedVariant([2], encodedVariantExistence); // false
187 changes: 187 additions & 0 deletions packages/hydrogen-react/src/optionValueDecoder.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import {describe, expect, it} from 'vitest';
import {
decodeEncodedVariant,
isOptionValueCombinationInEncodedVariant,
} from './optionValueDecoder.js';

describe('isOptionValueCombinationInEncodedVariant', () => {
it('returns true when target option values are present in encoded option values', () => {
const MOCK_ENCODED_OPTION_VALUES = 'v1_0:0:0,,1:1:1,,2:2:2,,';

expect(
isOptionValueCombinationInEncodedVariant(
[0, 0, 0],
MOCK_ENCODED_OPTION_VALUES,
),
).toBe(true);
});

it('returns true when a partial target option value is present in encoded option values', () => {
const MOCK_ENCODED_OPTION_VALUES = 'v1_0:0:0,,1:1:1,,2:2:2,,';

expect(
isOptionValueCombinationInEncodedVariant([0], MOCK_ENCODED_OPTION_VALUES),
).toBe(true);

expect(
isOptionValueCombinationInEncodedVariant(
[2, 2],
MOCK_ENCODED_OPTION_VALUES,
),
).toBe(true);
});

it('returns false when target option values are not present in encoded option values', () => {
const MOCK_ENCODED_OPTION_VALUES = 'v1_0:0:0,,1:1:1,,2:2:2,,';

expect(
isOptionValueCombinationInEncodedVariant(
[0, 0, 1],
MOCK_ENCODED_OPTION_VALUES,
),
).toBe(false);
});

it('returns false if no target option values are passed', () => {
const MOCK_ENCODED_OPTION_VALUES = 'v1_0:0:0,,1:1:1,,2:2:2,,';

expect(
isOptionValueCombinationInEncodedVariant([], MOCK_ENCODED_OPTION_VALUES),
).toBe(false);
});
});

describe('decodeEncodedVariant', () => {
describe('v1', () => {
const VERSION_PREFIX = 'v1_';

it('it correctly decodes a set of 1-dimensional arrays with no gaps in the number sequence', () => {
expect(decodeEncodedVariant(`${VERSION_PREFIX}0-9`)).toStrictEqual([
[0],
[1],
[2],
[3],
[4],
[5],
[6],
[7],
[8],
[9],
]);
});

it('it correctly decodes a set of 1-dimensional arrays with gaps in the number sequence', () => {
expect(
decodeEncodedVariant(`${VERSION_PREFIX}0-2 4-6 8-9`),
).toStrictEqual([[0], [1], [2], [4], [5], [6], [8], [9]]);
});

it('it correctly decodes a set of 2-dimensional arrays with no gaps in the number sequence', () => {
expect(
decodeEncodedVariant(`${VERSION_PREFIX}0:0-2,1:0-2,2:0-2,`),
).toStrictEqual([
[0, 0],
[0, 1],
[0, 2],
[1, 0],
[1, 1],
[1, 2],
[2, 0],
[2, 1],
[2, 2],
]);
});

it('it correctly decodes a set of 2-dimensional arrays with gaps in the number sequence', () => {
expect(
decodeEncodedVariant(`${VERSION_PREFIX}0:0-1,1:0 2,2:1-2,`),
).toStrictEqual([
[0, 0],
[0, 1],
[1, 0],
[1, 2],
[2, 1],
[2, 2],
]);
});

it('it correctly decodes a set of 3-dimensional arrays with no gaps in the number sequence', () => {
const output = generateCombinations(3, 3);
expect(
decodeEncodedVariant(
`${VERSION_PREFIX}0:0:0-2,1:0-2,2:0-2,,1:0:0-2,1:0-2,2:0-2,,2:0:0-2,1:0-2,2:0-2,,`,
),
).toStrictEqual(output);
});

it('it correctly decodes a set of 3-dimensional arrays with gaps in the number sequence', () => {
const output = generateCombinations(3, 3, [
[1, 0, 1],
[2, 2, 2],
[0, 2, 2],
]);
expect(
decodeEncodedVariant(
`${VERSION_PREFIX}0:0:0-2,1:0-2,2:0-1,,1:0:0 2,1:0-2,2:0-2,,2:0:0-2,1:0-2,2:0-1,`,
),
).toStrictEqual(output);
});

it('it correctly decodes a set of 4-dimensional arrays with no gaps in the number sequence', () => {
const output = generateCombinations(4, 3);
expect(
decodeEncodedVariant(
`${VERSION_PREFIX}"0:0:0:0-2,1:0-2,2:0-2,,1:0:0-2,1:0-2,2:0-2,,2:0:0-2,1:0-2,2:0-2,,,1:0:0:0-2,1:0-2,2:0-2,,1:0:0-2,1:0-2,2:0-2,,2:0:0-2,1:0-2,2:0-2,,,2:0:0:0-2,1:0-2,2:0-2,,1:0:0-2,1:0-2,2:0-2,,2:0:0-2,1:0-2,2:0-2,,,"`,
),
).toStrictEqual(output);
});

it('it correctly decodes a set of 4-dimensional arrays with gaps in the number sequence', () => {
const output = generateCombinations(4, 3, [
[1, 0, 1, 0],
[2, 2, 2, 0],
[0, 2, 2, 1],
]);
expect(
decodeEncodedVariant(
`${VERSION_PREFIX}0:0:0:0-2,1:0-2,2:0-2,,1:0:0-2,1:0-2,2:0-2,,2:0:0-2,1:0-2,2:0 2,,,1:0:0:0-2,1:1-2,2:0-2,,1:0:0-2,1:0-2,2:0-2,,2:0:0-2,1:0-2,2:0-2,,,2:0:0:0-2,1:0-2,2:0-2,,1:0:0-2,1:0-2,2:0-2,,2:0:0-2,1:0-2,2:1-2,,,`,
),
).toStrictEqual(output);
});
});
});

// generate all possible option value combos of a given depth and width
function generateCombinations(
depth: number,
width: number,
exclusions: number[][] = [],
): number[][] {
const input: number[][] = [];

function isInExclusions(array: number[]): boolean {
return exclusions.some(
(exclusion) =>
exclusion.length === array.length &&
exclusion.every((value, index) => value === array[index]),
);
}

function generate(currentArray: number[]): void {
if (currentArray.length === depth) {
if (!isInExclusions(currentArray)) {
input.push([...currentArray]);
}
return;
}

for (let i = 0; i < width; i++) {
currentArray.push(i);
generate(currentArray);
currentArray.pop();
}
}

generate([]);
return input;
}
Loading

0 comments on commit 76cd4f9

Please sign in to comment.