👁️
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

failable compilation

+135 -64
+75 -44
src/lib/search/fields.ts
··· 9 9 } from "@/lib/deck-validation/card-utils"; 10 10 import type { Card } from "../scryfall-types"; 11 11 import { compareColors } from "./colors"; 12 - import type { ComparisonOp, FieldName, FieldValue } from "./types"; 12 + import type { 13 + ComparisonOp, 14 + CompileError, 15 + FieldName, 16 + FieldValue, 17 + Result, 18 + Span, 19 + } from "./types"; 20 + import { err, ok } from "./types"; 13 21 14 22 /** 15 23 * Card predicate function type ··· 65 73 field: FieldName, 66 74 operator: ComparisonOp, 67 75 value: FieldValue, 68 - ): CardPredicate { 76 + span: Span, 77 + ): Result<CardPredicate, CompileError> { 69 78 switch (field) { 70 79 // Text fields 71 80 case "name": 72 - return compileTextField((c) => c.name, operator, value); 81 + return ok(compileTextField((c) => c.name, operator, value)); 73 82 74 83 case "type": 75 - return compileTextField((c) => c.type_line, operator, value); 84 + return ok(compileTextField((c) => c.type_line, operator, value)); 76 85 77 86 case "oracle": 78 - return compileOracleText(operator, value); 87 + return ok(compileOracleText(operator, value)); 79 88 80 89 // Color fields 81 90 case "color": 82 - return compileColorField((c) => c.colors, operator, value); 91 + return ok(compileColorField((c) => c.colors, operator, value)); 83 92 84 93 case "identity": 85 94 // Numeric comparison: id>1 means "more than 1 color in identity" 86 95 if (value.kind === "number") { 87 - return createOrderedMatcher( 88 - (card) => card.color_identity?.length ?? 0, 89 - value.value, 90 - operator, 96 + return ok( 97 + createOrderedMatcher( 98 + (card) => card.color_identity?.length ?? 0, 99 + value.value, 100 + operator, 101 + ), 91 102 ); 92 103 } 93 - return compileColorField((c) => c.color_identity, operator, value); 104 + return ok(compileColorField((c) => c.color_identity, operator, value)); 94 105 95 106 // Mana fields 96 107 case "mana": 97 - return compileTextField((c) => c.mana_cost, operator, value); 108 + return ok(compileTextField((c) => c.mana_cost, operator, value)); 98 109 99 110 case "manavalue": 100 - return compileNumericField((c) => c.cmc, operator, value); 111 + return ok(compileNumericField((c) => c.cmc, operator, value)); 101 112 102 113 // Stats 103 114 case "power": 104 - return compileStatField((c) => c.power, operator, value); 115 + return ok(compileStatField((c) => c.power, operator, value)); 105 116 106 117 case "toughness": 107 - return compileStatField((c) => c.toughness, operator, value); 118 + return ok(compileStatField((c) => c.toughness, operator, value)); 108 119 109 120 case "loyalty": 110 - return compileStatField((c) => c.loyalty, operator, value); 121 + return ok(compileStatField((c) => c.loyalty, operator, value)); 111 122 112 123 case "defense": 113 - return compileStatField((c) => c.defense, operator, value); 124 + return ok(compileStatField((c) => c.defense, operator, value)); 114 125 115 126 // Keywords 116 127 case "keyword": 117 - return compileKeyword(operator, value); 128 + return ok(compileKeyword(operator, value)); 118 129 119 130 // Set/printing (discrete fields use exact match for ':') 120 131 case "set": 121 - return compileTextField((c) => c.set, operator, value, true); 132 + return ok(compileTextField((c) => c.set, operator, value, true)); 122 133 123 134 case "settype": 124 - return compileTextField((c) => c.set_type, operator, value, true); 135 + return ok(compileTextField((c) => c.set_type, operator, value, true)); 125 136 126 137 case "layout": 127 - return compileTextField((c) => c.layout, operator, value, true); 138 + return ok(compileTextField((c) => c.layout, operator, value, true)); 128 139 129 140 case "frame": 130 - return compileTextField((c) => c.frame, operator, value, true); 141 + return ok(compileTextField((c) => c.frame, operator, value, true)); 131 142 132 143 case "border": 133 - return compileTextField((c) => c.border_color, operator, value, true); 144 + return ok(compileTextField((c) => c.border_color, operator, value, true)); 134 145 135 146 case "number": 136 - return compileTextField((c) => c.collector_number, operator, value); 147 + return ok(compileTextField((c) => c.collector_number, operator, value)); 137 148 138 149 case "rarity": 139 - return compileRarity(operator, value); 150 + return ok(compileRarity(operator, value)); 140 151 141 152 case "artist": 142 - return compileTextField((c) => c.artist, operator, value); 153 + return ok(compileTextField((c) => c.artist, operator, value)); 143 154 144 155 // Legality 145 156 case "format": 146 - return compileFormat(operator, value); 157 + return ok(compileFormat(operator, value)); 147 158 148 159 case "banned": 149 - return compileLegality("banned", value); 160 + return ok(compileLegality("banned", value)); 150 161 151 162 case "restricted": 152 - return compileLegality("restricted", value); 163 + return ok(compileLegality("restricted", value)); 153 164 154 165 // Misc 155 166 case "game": 156 - return compileGame(operator, value); 167 + return ok(compileGame(operator, value)); 157 168 158 169 case "in": 159 - return compileIn(operator, value); 170 + return ok(compileIn(operator, value)); 160 171 161 172 case "produces": 162 - return compileProduces(operator, value); 173 + return ok(compileProduces(operator, value)); 163 174 164 175 case "year": 165 - return compileYear(operator, value); 176 + return ok(compileYear(operator, value)); 166 177 167 178 case "date": 168 - return compileDate(operator, value); 179 + return ok(compileDate(operator, value)); 169 180 170 181 case "lang": 171 - return compileTextField((c) => c.lang, operator, value, true); 182 + return ok(compileTextField((c) => c.lang, operator, value, true)); 172 183 173 184 // Boolean predicates 174 185 case "is": 175 - return compileIs(value); 186 + return compileIs(value, span); 176 187 177 188 case "not": 178 - return compileNot(value); 189 + return compileNot(value, span); 179 190 180 191 default: 181 - return () => false; 192 + return ok(() => false); 182 193 } 183 194 } 184 195 ··· 1015 1026 }; 1016 1027 1017 1028 /** 1029 + * Set of valid is: predicate names (for autocomplete) 1030 + */ 1031 + export const IS_PREDICATE_NAMES = new Set(Object.keys(IS_PREDICATES)); 1032 + 1033 + /** 1018 1034 * Compile is: predicate 1019 1035 */ 1020 - function compileIs(value: FieldValue): CardPredicate { 1036 + function compileIs( 1037 + value: FieldValue, 1038 + span: Span, 1039 + ): Result<CardPredicate, CompileError> { 1021 1040 if (value.kind !== "string") { 1022 - return () => false; 1041 + return err({ message: "is: requires a text value", span }); 1023 1042 } 1024 1043 1025 1044 const predicate = IS_PREDICATES[value.value.toLowerCase()]; 1026 - return predicate ?? (() => false); 1045 + if (!predicate) { 1046 + return err({ 1047 + message: `'${value.value}' is not a valid is: predicate`, 1048 + span, 1049 + }); 1050 + } 1051 + return ok(predicate); 1027 1052 } 1028 1053 1029 1054 /** 1030 1055 * Compile not: predicate (negated is:) 1031 1056 */ 1032 - function compileNot(value: FieldValue): CardPredicate { 1033 - const isPredicate = compileIs(value); 1034 - return (card) => !isPredicate(card); 1057 + function compileNot( 1058 + value: FieldValue, 1059 + span: Span, 1060 + ): Result<CardPredicate, CompileError> { 1061 + const isResult = compileIs(value, span); 1062 + if (!isResult.ok) { 1063 + return isResult; 1064 + } 1065 + return ok((card) => !isResult.value(card)); 1035 1066 }
+15 -6
src/lib/search/index.ts
··· 13 13 import type { Card } from "../scryfall-types"; 14 14 import { type CardPredicate, compile } from "./matcher"; 15 15 import { parse } from "./parser"; 16 - import type { ParseError, Result, SearchNode } from "./types"; 16 + import type { CompileError, ParseError, Result, SearchNode } from "./types"; 17 17 18 18 /** 19 19 * Compiled search query ··· 26 26 } 27 27 28 28 /** 29 + * Search error - either a parse error or a compile error 30 + */ 31 + export type SearchError = ParseError | CompileError; 32 + 33 + /** 29 34 * Parse and compile a Scryfall search query 30 35 * 31 - * Returns a Result with either a compiled search or a parse error. 36 + * Returns a Result with either a compiled search or a parse/compile error. 32 37 */ 33 - export function search(query: string): Result<CompiledSearch> { 38 + export function search(query: string): Result<CompiledSearch, SearchError> { 34 39 const parseResult = parse(query); 35 40 36 41 if (!parseResult.ok) { ··· 38 43 } 39 44 40 45 const ast = parseResult.value; 41 - const match = compile(ast); 46 + const compileResult = compile(ast); 47 + 48 + if (!compileResult.ok) { 49 + return compileResult; 50 + } 42 51 43 52 return { 44 53 ok: true, 45 - value: { match, ast }, 54 + value: { match: compileResult.value, ast }, 46 55 }; 47 56 } 48 57 ··· 96 105 } 97 106 98 107 // Re-export types 99 - export type { SearchNode, Result, ParseError, CardPredicate }; 108 + export type { SearchNode, Result, ParseError, CompileError, CardPredicate }; 100 109 export type { CompiledSearch as SearchResult }; 101 110 102 111 export { describeQuery } from "./describe";
+36 -14
src/lib/search/matcher.ts
··· 5 5 */ 6 6 7 7 import { type CardPredicate, compileField } from "./fields"; 8 - import type { SearchNode } from "./types"; 8 + import type { CompileError, Result, SearchNode } from "./types"; 9 + import { ok } from "./types"; 9 10 10 11 // Re-export CardPredicate for convenience 11 12 export type { CardPredicate }; ··· 13 14 /** 14 15 * Compile an AST node into a card predicate function 15 16 */ 16 - export function compile(node: SearchNode): CardPredicate { 17 + export function compile(node: SearchNode): Result<CardPredicate, CompileError> { 17 18 switch (node.type) { 18 19 case "AND": 19 20 return compileAnd(node.children); ··· 25 26 return compileNot(node.child); 26 27 27 28 case "FIELD": 28 - return compileField(node.field, node.operator, node.value); 29 + return compileField(node.field, node.operator, node.value, node.span); 29 30 30 31 case "NAME": 31 - return compileName(node.value, node.pattern); 32 + return ok(compileName(node.value, node.pattern)); 32 33 33 34 case "EXACT_NAME": 34 - return compileExactName(node.value); 35 + return ok(compileExactName(node.value)); 35 36 } 36 37 } 37 38 38 39 /** 39 40 * Compile AND node - all children must match 40 41 */ 41 - function compileAnd(children: SearchNode[]): CardPredicate { 42 - const predicates = children.map(compile); 43 - return (card) => predicates.every((p) => p(card)); 42 + function compileAnd( 43 + children: SearchNode[], 44 + ): Result<CardPredicate, CompileError> { 45 + const predicates: CardPredicate[] = []; 46 + for (const child of children) { 47 + const result = compile(child); 48 + if (!result.ok) { 49 + return result; 50 + } 51 + predicates.push(result.value); 52 + } 53 + return ok((card) => predicates.every((p) => p(card))); 44 54 } 45 55 46 56 /** 47 57 * Compile OR node - any child must match 48 58 */ 49 - function compileOr(children: SearchNode[]): CardPredicate { 50 - const predicates = children.map(compile); 51 - return (card) => predicates.some((p) => p(card)); 59 + function compileOr( 60 + children: SearchNode[], 61 + ): Result<CardPredicate, CompileError> { 62 + const predicates: CardPredicate[] = []; 63 + for (const child of children) { 64 + const result = compile(child); 65 + if (!result.ok) { 66 + return result; 67 + } 68 + predicates.push(result.value); 69 + } 70 + return ok((card) => predicates.some((p) => p(card))); 52 71 } 53 72 54 73 /** 55 74 * Compile NOT node - child must not match 56 75 */ 57 - function compileNot(child: SearchNode): CardPredicate { 58 - const predicate = compile(child); 59 - return (card) => !predicate(card); 76 + function compileNot(child: SearchNode): Result<CardPredicate, CompileError> { 77 + const result = compile(child); 78 + if (!result.ok) { 79 + return result; 80 + } 81 + return ok((card) => !result.value(card)); 60 82 } 61 83 62 84 /**
+9
src/lib/search/types.ts
··· 269 269 } 270 270 271 271 /** 272 + * Compile error (semantic error during AST compilation) 273 + * Doesn't include input since the caller has it 274 + */ 275 + export interface CompileError { 276 + message: string; 277 + span: Span; 278 + } 279 + 280 + /** 272 281 * Result type for fallible operations 273 282 */ 274 283 export type Result<T, E = ParseError> =