All files / src/utils/typeGuards productbuilder.ts

100% Statements 50/50
100% Branches 9/9
100% Functions 4/4
100% Lines 50/50

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 1771x 1x   1x 1x 1x   1x 1x 1x 1x 1x 1x 1x     1x 1x 1x 1x 1x 1x 1x 1x   1x 1x 1x 1x 1x 1x 1x 1x 1x   1x                                                   1x 1x 199x 199x   1x                                                                                           1x 1x 59x 59x   1x                                                   1x 1x 111x 111x   1x                                           1x 1x 29x   29x 29x  
import { AVAILABILITY, type Availability } from '@/constants/common';
import * as v from 'valibot';
 
// `v.picklist` accepts a readonly array of literal values, so `Object.values`
// can be passed directly (no non-empty-tuple cast needed as with `z.enum`).
const availabilityValues = Object.values(AVAILABILITY);
 
// Valibot schema that accepts any string whose lowercased form matches an
// `AVAILABILITY` enum value. Non-string inputs are lowercased-through untouched
// and then rejected by the picklist.
const availabilitySchema = v.pipe(
  v.unknown(),
  v.transform((input) => (typeof input === 'string' ? input.toLowerCase() : input)),
  v.picklist(availabilityValues),
);
 
// Valibot schema for the subset of `Variant` fields that are type-checked here. All
// fields are optional because a variant may inherit values from its parent product.
// `looseObject` preserves (and does not reject) any additional keys the caller includes.
const variantSchema = v.looseObject({
  title: v.optional(v.string()),
  price: v.optional(v.number()),
  quantity: v.optional(v.number()),
});
 
// Valibot schema for a `ProductImage` entry: a string `href` and a `type` that is
// either "image" or "thumbnail". `altText` is optional. `looseObject` preserves
// any extra keys. The `href` is validated for shape only (a string); the builder
// resolves it to an absolute URL afterwards.
const productImageSchema = v.looseObject({
  href: v.string(),
  type: v.picklist(['image', 'thumbnail']),
  altText: v.optional(v.string()),
});
 
/**
 * Type guard to validate if a value is a valid availability status.
 * Checks if the value is a string that matches one of the predefined AVAILABILITY enum values.
 * Matching is case-insensitive — input is lowercased before comparison.
 * @param availability - The value to check
 * @returns Type predicate indicating if the value is a valid AVAILABILITY enum value
 * @category Typeguards
 * @example
 * ```typescript
 * // Valid availability values
 * if (isAvailability('IN_STOCK')) {
 *   console.log('Product is in stock');
 * }
 * if (isAvailability('OUT_OF_STOCK')) {
 *   console.log('Product is out of stock');
 * }
 *
 * // Invalid availability values
 * if (!isAvailability('available')) {
 *   console.log('Invalid availability status');
 * }
 * if (!isAvailability(123)) {
 *   console.log('Availability must be a string');
 * }
 * ```
 * @source
 */
export function isAvailability(availability: unknown): availability is Availability {
  return v.safeParse(availabilitySchema, availability).success;
}
 
/**
 * Type guard to validate if a value is a valid product variant.
 * Checks for the presence and correct types of variant properties.
 * A variant can be partial as it may inherit some properties from the parent product
 * (such as uom, currency, URL, etc.).
 * @param variant - The variant object to validate
 * @returns Type predicate indicating if the value is a valid partial Variant
 * @category Typeguards
 * @example
 * ```typescript
 * // Valid variant with all properties
 * const completeVariant = {
 *   title: "Sodium Chloride 500g",
 *   price: 29.99,
 *   quantity: 500
 * };
 * if (isValidVariant(completeVariant)) {
 *   console.log('Valid complete variant:', completeVariant.title);
 * }
 *
 * // Valid partial variant (inheriting some properties from parent)
 * const partialVariant = {
 *   price: 39.99,
 *   quantity: 1000
 *   // title and other properties inherited from parent product
 * };
 * if (isValidVariant(partialVariant)) {
 *   console.log('Valid partial variant');
 * }
 *
 * // Invalid variant (wrong types)
 * const invalidVariant = {
 *   title: "Sodium Chloride",
 *   price: "29.99", // Should be number
 *   quantity: "500"  // Should be number
 * };
 * if (!isValidVariant(invalidVariant)) {
 *   console.log('Invalid variant - wrong property types');
 * }
 *
 * // Invalid variant (null/undefined)
 * if (!isValidVariant(null)) {
 *   console.log('Invalid variant - null value');
 * }
 * ```
 * @source
 */
export function isValidVariant(variant: unknown): variant is Partial<Variant> {
  return v.safeParse(variantSchema, variant).success;
}
 
/**
 * Type guard to validate if a value is a valid product image entry.
 * Checks for a string `href` and a `type` of either "image" or "thumbnail".
 * The `href` is only shape-checked here; the builder resolves it to an absolute
 * URL (and drops the entry when it can't).
 * @param value - The value to check
 * @returns Type predicate indicating if the value is a valid ProductImage
 * @category Typeguards
 * @example
 * ```typescript
 * // Valid image entry
 * if (isProductImage({ href: "https://example.com/a.jpg", type: "image" })) {
 *   console.log('Valid product image');
 * }
 *
 * // Invalid — unknown type
 * if (!isProductImage({ href: "https://example.com/a.jpg", type: "banner" })) {
 *   console.log('Not a valid product image');
 * }
 *
 * // Invalid — missing href
 * if (!isProductImage({ type: "thumbnail" })) {
 *   console.log('Not a valid product image');
 * }
 * ```
 * @source
 */
export function isProductImage(value: unknown): value is ProductImage {
  return v.safeParse(productImageSchema, value).success;
}
 
/**
 * Type guard for a cached product-data record — the plain object produced by
 * `ProductBuilder.dump()` and round-tripped through the product-detail cache.
 * Narrows an `unknown` (or `Record<string, unknown>`) cache read to `Partial<T>`
 * so it can be handed to `ProductBuilder.setData` without an assertion.
 *
 * The check is intentionally structural (a non-null, non-array object) rather
 * than a full-schema validation: `setData` already routes every key through its
 * own validating setter and silently drops any field that fails, so validating
 * individual fields here would only risk rejecting an otherwise-usable product.
 *
 * @param value - The value read back from the product-detail cache
 * @returns Type predicate indicating the value is a `Partial<T>` record
 * @category Typeguards
 * @example
 * ```typescript
 * const cached = await this.cache.getCachedProductData(key);
 * if (isCachedProductData<Product>(cached)) {
 *   product.setData(cached); // cached is Partial<Product>
 * }
 * ```
 * @source
 */
export function isCachedProductData<T extends Product = Product>(
  value: unknown,
): value is Partial<T> {
  return typeof value === 'object' && value !== null && !Array.isArray(value);
}