Skip to content

Commit

Permalink
ingredient & result zod validation
Browse files Browse the repository at this point in the history
  • Loading branch information
PssbleTrngle committed Oct 11, 2023
1 parent 3c9d9ae commit 447a0dd
Show file tree
Hide file tree
Showing 37 changed files with 239 additions and 1,887 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"node-stream-zip": "^1.15.0",
"prettier": "^2.7.1",
"prismarine-nbt": "^2.2.1",
"typescript": "^4.8.2"
"typescript": "^4.8.2",
"zod": "^3.22.4"
}
}
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

123 changes: 57 additions & 66 deletions src/common/ingredient.ts
Original file line number Diff line number Diff line change
@@ -1,99 +1,90 @@
import { TagRegistry, TagRegistryHolder } from '../loader/tags'
import { createId, encodeId, Id, IdInput, NormalizedId, TagInput } from './id'
import { createId, encodeId, Id, NormalizedId } from './id'
import { Logger } from '../logger'
import { resolveCommonTest } from './predicates'
import { Block, BlockSchema, Fluid, FluidSchema, Item, ItemSchema } from './result'
import zod from 'zod'
import { exists } from '@pssbletrngle/pack-resolver'
import { IllegalShapeError } from '../error'

type Item = Readonly<{ item: string }>
type ItemTag = Readonly<{ tag: string }>
export const ItemTagSchema = zod.object({
tag: zod.string(),
})

type Fluid = Readonly<{ fluid: string }>
type FluidTag = Readonly<{ fluidTag: string }>
export type ItemTag = zod.infer<typeof ItemTagSchema>

export type Block = Readonly<{
block: string
weight?: number
}>
export const FluidTagSchema = zod.object({
fluidTag: zod.string(),
})

export type BlockTag = Readonly<{
blockTag: string
weight?: number
}>
export type FluidTag = zod.infer<typeof FluidTagSchema>

export type Ingredient = ItemTag | Item | Fluid | FluidTag | Block | BlockTag
export const BlockTagSchema = zod.object({
blockTag: zod.string(),
weight: zod.number().optional(),
})

export type BlockTag = zod.infer<typeof BlockTagSchema>

export type Ingredient = Item | ItemTag | Fluid | FluidTag | Block | BlockTag | IngredientInput[]
export type IngredientInput = Ingredient | string

export function createIngredient(input: IngredientInput): Ingredient {
if (!input) throw new IllegalShapeError('result input may not be null')

if (typeof input === 'string') {
if (input.startsWith('#')) return { tag: input.substring(1) }
else return { item: input }
}

return input
}

type ItemStack = Item &
Readonly<{
count?: number
}>
if (Array.isArray(input)) {
return input.map(createIngredient)
}

type FluidStack = Fluid &
Readonly<{
amount: number
}>
if (typeof input === 'object') {
if ('item' in input) return ItemSchema.parse(input)
if ('fluid' in input) return FluidSchema.parse(input)
if ('block' in input) return BlockSchema.parse(input)

export type Result = ItemStack | FluidStack | Block
export type ResultInput = Result | string
if ('tag' in input) return ItemTagSchema.parse(input)
if ('fluidTag' in input) return FluidTagSchema.parse(input)
if ('blockTag' in input) return BlockTagSchema.parse(input)
}

export function createResult(input: ResultInput): Result {
if (typeof input === 'string') return { item: input }
return input
throw new IllegalShapeError('unknown ingredient shape', input)
}

export type Predicate<T> = (value: T) => boolean
export type Predicate<T> = (value: T, logger: Logger) => boolean
export type CommonTest<T> = RegExp | Predicate<T> | T
export type IngredientTest = CommonTest<Ingredient> | NormalizedId

export function resolveCommonTest<TEntry, TId extends string>(
test: CommonTest<NormalizedId<TId>>,
resolve: (value: TEntry) => NormalizedId<TId>,
tags?: TagRegistry
): Predicate<TEntry> {
if (typeof test === 'function') {
return it => test(resolve(it))
} else if (test instanceof RegExp) {
return ingredient => {
return test.test(resolve(ingredient))
}
} else if (test.startsWith('#')) {
return ingredient => {
const id = resolve(ingredient)
if (id.startsWith('#') && test === id) return true
else if (tags) return tags.contains(test as TagInput, id) ?? false
else throw new Error('Cannot parse ID test without tags')
}
} else {
return ingredient => {
return test === resolve(ingredient)
}
}
}

export function resolveIDTest<T extends NormalizedId>(test: CommonTest<T>, tags?: TagRegistry): Predicate<IdInput<T>> {
return resolveCommonTest(test, it => encodeId<T>(it), tags)
}

export function resolveIdIngredientTest(
test: NormalizedId | RegExp,
tags: TagRegistry,
logger: Logger,
idSupplier: (it: Ingredient) => Id | null
): Predicate<IngredientInput> {
function resolveIds(it: IngredientInput): Id[] {
if (Array.isArray(it)) {
return it.flatMap(resolveIds)
} else {
return [idSupplier(createIngredient(it))].filter(exists)
}
}

return resolveCommonTest(
test,
ingredient => {
const id = idSupplier(createIngredient(ingredient))
if (id) return encodeId(id)
logger.warn('unknown ingredient shape:', ingredient)
return '__ignored' as NormalizedId
input => {
try {
return resolveIds(input).map(encodeId)
} catch (error) {
if (error instanceof IllegalShapeError) {
logger.warn((error as Error).message)
return []
} else {
throw error
}
}
},
tags
)
Expand Down Expand Up @@ -127,7 +118,7 @@ export function resolveIngredientTest(
}

if (typeof test === 'function') {
return it => test(createIngredient(it))
return (it, logger) => test(createIngredient(it), logger)
}

if ('tag' in test)
Expand Down
33 changes: 33 additions & 0 deletions src/common/predicates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { encodeId, IdInput, NormalizedId, TagInput } from './id'
import { TagRegistry } from '../loader/tags'
import { CommonTest, Predicate } from './ingredient'

export function resolveCommonTest<TEntry, TId extends string>(
test: CommonTest<NormalizedId<TId>>,
resolve: (value: TEntry) => NormalizedId<TId>[],
tags?: TagRegistry
): Predicate<TEntry> {
if (typeof test === 'function') {
return it => resolve(it).some(test)
} else if (test instanceof RegExp) {
return ingredient => {
return resolve(ingredient).some(it => test.test(it))
}
} else if (test.startsWith('#')) {
return ingredient => {
return resolve(ingredient).some(id => {
if (id.startsWith('#') && test === id) return true
else if (tags) return tags.contains(test as TagInput, id) ?? false
else throw new Error('Cannot parse ID test without tags')
})
}
} else {
return ingredient => {
return resolve(ingredient).includes(test)
}
}
}

export function resolveIDTest<T extends NormalizedId>(test: CommonTest<T>, tags?: TagRegistry): Predicate<IdInput<T>> {
return resolveCommonTest(test, it => [encodeId<T>(it)], tags)
}
52 changes: 52 additions & 0 deletions src/common/result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import zod from 'zod'
import { IllegalShapeError } from '../error'

export const ItemSchema = zod.object({
item: zod.string(),
})

export type Item = zod.infer<typeof ItemSchema>

export const FluidSchema = zod.object({
fluid: zod.string(),
})

export type Fluid = zod.infer<typeof FluidSchema>

export const ItemStackSchema = ItemSchema.and(
zod.object({
count: zod.number().optional(),
})
)

export type ItemStack = zod.infer<typeof ItemStackSchema>

export const FluidStackSchema = FluidSchema.and(
zod.object({
amount: zod.number().optional(),
})
)

export type FluidStack = zod.infer<typeof FluidStackSchema>

export const BlockSchema = zod.object({
block: zod.string(),
weight: zod.number().optional(),
})

export type Block = zod.infer<typeof BlockSchema>

export type Result = ItemStack | FluidStack | Block
export type ResultInput = Result | string

export function createResult(input: ResultInput): Result {
if (!input) throw new IllegalShapeError('result input may not be null')

if (typeof input === 'string') return { item: input }

if ('item' in input) return ItemStackSchema.parse(input)
if ('fluid' in input) return FluidStackSchema.parse(input)
if ('block' in input) return BlockSchema.parse(input)

throw new IllegalShapeError(`unknown result shape`, input)
}
14 changes: 10 additions & 4 deletions src/emit/recipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@ import {
IngredientInput,
IngredientTest,
Predicate,
resolveIDTest,
resolveIngredientTest,
Result,
} from '../common/ingredient'
import RecipeRule from '../rule/recipe'
import { Logger } from '../logger'
import TagsLoader from '../loader/tags'
import { createId, Id, IdInput, NormalizedId } from '../common/id'
import { createId, encodeId, Id, IdInput, NormalizedId } from '../common/id'
import { Recipe } from '../parser/recipe'
import { RecipeDefinition } from '../schema/recipe'
import Registry from '../common/registry'
import { resolveIDTest } from '../common/predicates'
import { Result } from '../common/result'

type RecipeTest = Readonly<{
id?: CommonTest<NormalizedId>
Expand Down Expand Up @@ -82,7 +82,13 @@ export default class RecipeEmitter implements RecipeRules {

const path = this.recipePath(id)

const rules = this.rules.filter(it => it.matches(id, recipe))
const rules = this.rules.filter(it => {
try {
return it.matches(id, recipe)
} catch (error) {
this.logger.error(`Could not parse recipe ${encodeId(id)}: ${error}`)
}
})
if (rules.length === 0) return

const modified = rules.reduce<Recipe | null>((previous, rule) => previous && rule.modify(previous), recipe)
Expand Down
3 changes: 2 additions & 1 deletion src/emit/tags.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Acceptor } from '@pssbletrngle/pack-resolver'
import { toJson } from '../textHelper'
import { CommonTest, resolveIDTest } from '../common/ingredient'
import { CommonTest } from '../common/ingredient'
import { Logger } from '../logger'
import TagsLoader, { entryId, orderTagEntries, TagRegistry } from '../loader/tags'
import { TagDefinition, TagEntry } from '../schema/tag'
import { createId, Id, NormalizedId, TagInput } from '../common/id'
import Registry from '../common/registry'
import { resolveIDTest } from '../common/predicates'

export interface TagRules {
add(registry: string, id: `#${string}`, value: TagEntry): void
Expand Down
5 changes: 5 additions & 0 deletions src/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class IllegalShapeError extends Error {
constructor(message: string, readonly input?: any) {
super(input ? `${message}: ${JSON.stringify(input)}` : message)
}
}
19 changes: 12 additions & 7 deletions src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,23 @@ type LogMethods = {
}

export type Logger = LogMethods & {
group(): Logger
group(prefix?: string): Logger
}

export function wrapLogMethods(logMethods: LogMethods) {
return { ...logMethods, group: () => subLogger(logMethods) }
export function wrapLogMethods(logMethods: LogMethods): Logger {
return { ...logMethods, group: prefix => subLogger(logMethods, prefix) }
}

function subLogger(logger: LogMethods): Logger {
function grouped(prefix: string | undefined, message: Logable) {
if (prefix) return `${prefix}: ${message}`
return ` ${message}`
}

function subLogger(logger: LogMethods, prefix?: string): Logger {
return wrapLogMethods({
error: message => logger.error(` ${message}`),
warn: message => logger.warn(` ${message}`),
info: message => logger.info(` ${message}`),
error: (message, ...args) => logger.error(grouped(prefix, message), ...args),
warn: (message, ...args) => logger.warn(grouped(prefix, message), ...args),
info: (message, ...args) => logger.info(grouped(prefix, message), ...args),
})
}

Expand Down
3 changes: 2 additions & 1 deletion src/parser/recipe/botania/apothecary.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import RecipeParser, { Recipe, replace } from '..'
import { IngredientInput, Predicate, ResultInput } from '../../../common/ingredient'
import { IngredientInput, Predicate } from '../../../common/ingredient'
import { RecipeDefinition } from '../../../schema/recipe'
import { ResultInput } from '../../../common/result'

export type ApothecaryRecipeDefinition = RecipeDefinition &
Readonly<{
Expand Down
3 changes: 2 additions & 1 deletion src/parser/recipe/botania/brew.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import RecipeParser, { Recipe, replace } from '..'
import { IngredientInput, Predicate, ResultInput } from '../../../common/ingredient'
import { IngredientInput, Predicate } from '../../../common/ingredient'
import { RecipeDefinition } from '../../../schema/recipe'
import { ResultInput } from '../../../common/result'

export type BrewRecipeDefinition = RecipeDefinition &
Readonly<{
Expand Down
3 changes: 2 additions & 1 deletion src/parser/recipe/botania/elvenTrade.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import RecipeParser, { Recipe, replace } from '..'
import { IngredientInput, Predicate, ResultInput } from '../../../common/ingredient'
import { IngredientInput, Predicate } from '../../../common/ingredient'
import { RecipeDefinition } from '../../../schema/recipe'
import { ResultInput } from '../../../common/result'

export type ElvenTradeRecipeDefinition = RecipeDefinition &
Readonly<{
Expand Down
Loading

0 comments on commit 447a0dd

Please sign in to comment.