UNPKG

@apoyo/decoders

Version:
622 lines (610 loc) 22.4 kB
// src/Errors.ts var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => { ErrorCode2["REQUIRED"] = "required"; ErrorCode2["STRING"] = "string"; ErrorCode2["STRING_LENGTH"] = "string.length"; ErrorCode2["STRING_MIN"] = "string.min"; ErrorCode2["STRING_MAX"] = "string.max"; ErrorCode2["STRING_PATTERN"] = "string.pattern"; ErrorCode2["STRING_EMAIL"] = "string.email"; ErrorCode2["STRING_UUID"] = "string.uuid"; ErrorCode2["STRING_EQUALS"] = "string.equals"; ErrorCode2["STRING_ONE_OF"] = "string.oneOf"; ErrorCode2["STRING_DATE"] = "string.date"; ErrorCode2["STRING_DATETIME"] = "string.datetime"; ErrorCode2["DATE"] = "date"; ErrorCode2["DATE_STRICT"] = "date.strict"; ErrorCode2["DATE_MIN"] = "date.min"; ErrorCode2["DATE_MAX"] = "date.max"; ErrorCode2["NUMBER"] = "number"; ErrorCode2["NUMBER_STRICT"] = "number.strict"; ErrorCode2["NUMBER_FROM_STRING"] = "number.fromString"; ErrorCode2["NUMBER_MIN"] = "number.min"; ErrorCode2["NUMBER_MAX"] = "number.max"; ErrorCode2["INT"] = "int"; ErrorCode2["INT_STRICT"] = "int.strict"; ErrorCode2["INT_FROM_STRING"] = "int.fromString"; ErrorCode2["INT_MIN"] = "int.min"; ErrorCode2["INT_MAX"] = "int.max"; ErrorCode2["BOOL"] = "bool"; ErrorCode2["BOOL_STRICT"] = "bool.strict"; ErrorCode2["BOOL_FROM_STRING"] = "bool.fromString"; ErrorCode2["BOOL_FROM_NUMBER"] = "bool.fromNumber"; ErrorCode2["BOOL_EQUALS"] = "bool.equals"; ErrorCode2["ARRAY"] = "array"; ErrorCode2["ARRAY_NON_EMPTY"] = "array.nonEmpty"; ErrorCode2["ARRAY_LENGTH"] = "array.length"; ErrorCode2["ARRAY_MIN"] = "array.min"; ErrorCode2["ARRAY_MAX"] = "array.max"; ErrorCode2["DICT"] = "dict"; ErrorCode2["ENUM"] = "enum.native"; ErrorCode2["ENUM_LITERAL"] = "enum.literal"; ErrorCode2["ENUM_IS_IN"] = "enum.isIn"; return ErrorCode2; })(ErrorCode || {}); // src/Decoder.ts import { pipe as pipe2, Result } from "@apoyo/std"; // src/DecodeError.ts import { Arr, pipe, Tree } from "@apoyo/std"; var DecodeErrorTag = /* @__PURE__ */ ((DecodeErrorTag2) => { DecodeErrorTag2["VALUE"] = "DE.Value"; DecodeErrorTag2["ARRAY"] = "DE.ArrayLike"; DecodeErrorTag2["INDEX"] = "DE.Index"; DecodeErrorTag2["OBJECT"] = "DE.ObjectLike"; DecodeErrorTag2["KEY"] = "DE.Key"; DecodeErrorTag2["UNION"] = "DE.UnionLike"; DecodeErrorTag2["MEMBER"] = "DE.Member"; return DecodeErrorTag2; })(DecodeErrorTag || {}); var value = (value2, message, meta = {}) => ({ tag: "DE.Value" /* VALUE */, value: value2, message, meta }); var key = (key2, error) => ({ tag: "DE.Key" /* KEY */, key: key2, error }); var index = (index2, error) => ({ tag: "DE.Index" /* INDEX */, index: index2, error }); var member = (index2, error) => ({ tag: "DE.Member" /* MEMBER */, index: index2, error }); var array = (errors) => ({ tag: "DE.ArrayLike" /* ARRAY */, kind: "array", errors }); var object = (errors, name) => ({ tag: "DE.ObjectLike" /* OBJECT */, kind: "object", name, errors }); var union = (errors, name) => ({ tag: "DE.UnionLike" /* UNION */, kind: "union", name, errors }); var fold = (cases) => (err) => { switch (err.tag) { case "DE.Value" /* VALUE */: return cases.value(err); case "DE.ArrayLike" /* ARRAY */: return cases.array(err); case "DE.ObjectLike" /* OBJECT */: return cases.object(err); case "DE.UnionLike" /* UNION */: return cases.union(err); } }; var toTree = fold({ value: (err) => Tree.of(`cannot decode ${JSON.stringify(err.value)}: ${err.message}`), array: (err) => Tree.of(`${err.kind}`, pipe(err.errors, Arr.map((err2) => Tree.of(`index ${JSON.stringify(err2.index)}`, [err2.error].map(toTree))))), object: (err) => Tree.of(`${err.kind}${err.name ? " " + err.name : ""}`, pipe(err.errors, Arr.map((err2) => Tree.of(`property ${JSON.stringify(err2.key)}`, [err2.error].map(toTree))))), union: (err) => Tree.of(`${err.kind}${err.name ? " " + err.name : ""}`, pipe(err.errors, Arr.map((err2) => Tree.of(`member ${JSON.stringify(err2.index)}`, [err2.error].map(toTree))))) }); var draw = (e) => pipe(e, toTree, Tree.draw); var flatten = fold({ value: (err) => [ { value: err.value, message: err.message, meta: err.meta, path: [] } ], array: (err) => pipe(err.errors, Arr.chain((sub) => pipe(flatten(sub.error), Arr.map((error) => ({ ...error, path: [ { tag: "DE.ArrayLike" /* ARRAY */, kind: err.kind }, { tag: "DE.Index" /* INDEX */, index: sub.index }, ...error.path ] }))))), object: (err) => pipe(err.errors, Arr.chain((sub) => pipe(flatten(sub.error), Arr.map((error) => ({ ...error, path: [ { tag: "DE.ObjectLike" /* OBJECT */, kind: err.kind, name: err.name }, { tag: "DE.Key" /* KEY */, key: sub.key }, ...error.path ] }))))), union: (err) => pipe(err.errors, Arr.chain((sub) => pipe(flatten(sub.error), Arr.map((error) => ({ ...error, path: [ { tag: "DE.UnionLike" /* UNION */, kind: err.kind, name: err.name }, { tag: "DE.Member" /* MEMBER */, index: sub.index }, ...error.path ] }))))) }); var formatBy = (fn) => (e) => pipe(e, flatten, Arr.map(fn)); var getDescription = (err, separator = ", at ") => { const stack = pipe(err.path, Arr.filterMap((p) => { if (p.tag === "DE.ArrayLike" /* ARRAY */) { return `${p.kind}`; } if (p.tag === "DE.Index" /* INDEX */) { return `index ${p.index}`; } if (p.tag === "DE.ObjectLike" /* OBJECT */) { return `${p.kind}${p.name ? " " + p.name : ""}`; } if (p.tag === "DE.Key" /* KEY */) { return `property ${JSON.stringify(p.key)}`; } if (p.tag === "DE.UnionLike" /* UNION */) { return `${p.kind}${p.name ? " " + p.name : ""}`; } if (p.tag === "DE.Member" /* MEMBER */) { return `member ${p.index}`; } return void 0; })); return pipe([`cannot decode ${JSON.stringify(err.value)}: ${err.message}`, ...stack], Arr.join(separator)); }; var toPath = (path) => pipe(path, Arr.mapIndexed((item, i) => typeof item === "number" ? `[${item}]` : i > 0 ? `.${item}` : item), Arr.join("")); var getPath = (err) => { return pipe(err.path, Arr.filterMap((p) => p.tag === "DE.Index" /* INDEX */ ? p.index : p.tag === "DE.Key" /* KEY */ ? p.key : void 0), toPath); }; var getFormatted = (e) => ({ value: e.value, message: e.message, meta: e.meta, description: getDescription(e), path: getPath(e) }); var format = formatBy(getFormatted); var formatError = (e) => getDescription(e); var DecodeError = { value, key, index, member, array, object, union, fold, toTree, draw, flatten, getDescription, getPath, getFormatted, formatBy, format, formatError }; // src/Decoder.ts var create = (fn) => ({ decode: fn }); var fromGuard = (fn, message, meta) => create((input) => fn(input) ? Result.ok(input) : Result.ko(DecodeError.value(input, message, meta))); var parse = (fn) => (decoder) => create((input) => pipe2(input, validate(decoder), Result.chain(fn))); var chain = (fn) => (decoder) => pipe2(decoder, parse((value2) => pipe2(value2, validate(fn(value2))))); var map = (fn) => (decoder) => create((input) => pipe2(input, validate(decoder), Result.map(fn))); var mapError = (fn) => (decoder) => create((input) => pipe2(input, validate(decoder), Result.mapError((err) => fn(err, input)))); var withMessage = (msg, meta) => (decoder) => pipe2(decoder, mapError((_, input) => DecodeError.value(input, msg, meta))); var nullable = (decoder) => create((input) => input === null || input === void 0 ? Result.ok(null) : pipe2(input, validate(decoder))); var optional = (decoder) => create((input) => input === void 0 ? Result.ok(void 0) : pipe2(input, validate(decoder))); var required = (decoder) => create((input) => input === void 0 || input === null ? Result.ko(DecodeError.value(input, "input is required", { code: "required" /* REQUIRED */ })) : pipe2(input, validate(decoder))); var guard = (fn) => parse((input) => { const error = fn(input); return error !== void 0 ? Result.ko(error) : Result.ok(input); }); function filter(fn, message, meta = {}) { return parse((input) => fn(input) ? Result.ok(input) : Result.ko(DecodeError.value(input, message, meta))); } function reject(fn, message, meta = {}) { return parse((input) => !fn(input) ? Result.ok(input) : Result.ko(DecodeError.value(input, message, meta))); } var ref = (decoder) => decoder; function validate(decoder) { return (input) => decoder.decode(input); } var lazy = (fn) => create((input) => pipe2(input, validate(fn()))); function defaultValue(value2) { return (decoder) => pipe2(decoder, optional, map((input) => input === void 0 ? value2 : input)); } function union2(...members) { return create((input) => pipe2(members, Result.unionBy((member2, index2) => pipe2(input, Decoder.validate(member2), Result.mapError((err) => DecodeError.member(index2, err)))), Result.mapError(DecodeError.union))); } var any = create(Result.ok); var unknown = create(Result.ok); var Decoder = { create, fromGuard, map, mapError, withMessage, parse, chain, guard, filter, reject, nullable, optional, required, lazy, default: defaultValue, union: union2, ref, validate, unknown, any }; // src/TextDecoder.ts import { flow, pipe as pipe3, Str } from "@apoyo/std"; var REGEXP_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i; var REGEXP_EMAIL = /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/; var REGEXP_DATE = /^([0-9]{4}[-](0?[1-9]|1[0-2])[-](0?[1-9]|[12][0-9]|3[01]))$/; var REGEXP_DATETIME = /^([0-9]{4}[-](0?[1-9]|1[0-2])[-](0?[1-9]|[12][0-9]|3[01]))[T ]((0?[1-9]|[1][0-9]|2[0-3]):([0-5][0-9])(:([0-5][0-9])(\.\d+)?)?)Z?$/; var string = Decoder.fromGuard((input) => typeof input === "string", `value is not a string`, { code: "string" /* STRING */ }); var length = (len) => Decoder.filter((input) => input.length === len, `string should contain exactly ${len} characters`, { code: "string.length" /* STRING_LENGTH */, length: len }); var min = (minLength) => Decoder.filter((input) => input.length >= minLength, `string should contain at least ${minLength} characters`, { code: "string.min" /* STRING_MIN */, minLength }); var max = (maxLength) => Decoder.filter((input) => input.length <= maxLength, `string should contain at most ${maxLength} characters`, { code: "string.max" /* STRING_MAX */, maxLength }); var between = (minLength, maxLength) => flow(min(minLength), max(maxLength)); var varchar = (minLength, maxLength) => pipe3(string, between(minLength, maxLength)); var nullable2 = flow(Decoder.map((str) => str.length === 0 ? null : str), Decoder.nullable); var optional2 = flow(Decoder.map((str) => str.length === 0 ? void 0 : str), Decoder.optional); var trim = Decoder.map(Str.trim); var htmlEscape = Decoder.map(Str.htmlEscape); var pattern = (regexp, message = "string does not match the given pattern", meta = {}) => Decoder.filter((value2) => regexp.test(value2), message, { code: "string.pattern" /* STRING_PATTERN */, ...meta }); var date = pipe3(string, pattern(REGEXP_DATE, `string is not a date string`, { code: "string.date" /* STRING_DATE */ })); var datetime = pipe3(string, pattern(REGEXP_DATETIME, `string is not a datetime string`, { code: "string.datetime" /* STRING_DATETIME */ })); var email = pipe3(string, pattern(REGEXP_EMAIL, `string is not an email`, { code: "string.email" /* STRING_EMAIL */ })); var uuid = pipe3(string, pattern(REGEXP_UUID, `string is not an uuid`, { code: "string.uuid" /* STRING_UUID */ })); var equals = (value2) => pipe3(string, Decoder.filter((str) => str === value2, `string is not equal to value ${JSON.stringify(value2)}`, { code: "string.equals" /* STRING_EQUALS */ })); function oneOf(arr) { const set = new Set(arr); return pipe3(string, Decoder.filter((str) => set.has(str), `string is not included in the given values`, { code: "string.oneOf" /* STRING_ONE_OF */, values: arr })); } var TextDecoder = { string, length, min, max, between, trim, pattern, date, datetime, email, uuid, varchar, nullable: nullable2, optional: optional2, oneOf, equals, htmlEscape }; // src/NumberDecoder.ts import { flow as flow2, pipe as pipe4 } from "@apoyo/std"; var strict = Decoder.fromGuard((input) => typeof input === "number" && !Number.isNaN(input), `value is not a number`, { code: "number.strict" /* NUMBER_STRICT */ }); var fromString = pipe4(TextDecoder.string, Decoder.map(parseFloat), Decoder.chain(() => strict), Decoder.withMessage(`could not parse input string into a number`, { code: "number.fromString" /* NUMBER_FROM_STRING */ })); var number = pipe4(Decoder.union(strict, fromString), Decoder.withMessage(`value is not a number`, { code: "number" /* NUMBER */ })); var min2 = (minimum) => Decoder.filter((input) => input >= minimum, `number should be greater or equal than ${minimum}`, { code: "number.min" /* NUMBER_MIN */, minimum }); var max2 = (maximum) => Decoder.filter((input) => input <= maximum, `number should be lower or equal than ${maximum}`, { code: "number.max" /* NUMBER_MAX */, maximum }); var between2 = (minimum, maximum) => flow2(min2(minimum), max2(maximum)); var range = (minimum, maximum) => pipe4(number, between2(minimum, maximum)); var NumberDecoder = { strict, number, min: min2, max: max2, between: between2, range, fromString }; // src/IntegerDecoder.ts import { pipe as pipe5 } from "@apoyo/std"; var strict2 = pipe5(NumberDecoder.strict, Decoder.filter((nb) => nb % 1 === 0, `number is not a integer`, { code: "int.strict" /* INT_STRICT */ })); var fromString2 = pipe5(NumberDecoder.fromString, Decoder.chain(() => strict2), Decoder.withMessage(`could not parse input string into an integer`, { code: "int.fromString" /* INT_FROM_STRING */ })); var int = pipe5(Decoder.union(strict2, fromString2), Decoder.withMessage(`value is not a integer`, { code: "int" /* INT */ })); var min3 = NumberDecoder.min; var max3 = NumberDecoder.max; var between3 = NumberDecoder.between; var positive = pipe5(int, min3(0)); var range2 = (minimum, maximum) => pipe5(int, between3(minimum, maximum)); var IntegerDecoder = { strict: strict2, int, min: min3, max: max3, between: between3, range: range2, positive, fromString: fromString2 }; // src/BooleanDecoder.ts import { pipe as pipe6, Result as Result2 } from "@apoyo/std"; var TEXT_TRUE = /* @__PURE__ */ new Set(["true", "yes", "y", "1"]); var TEXT_FALSE = /* @__PURE__ */ new Set(["false", "no", "no", "0"]); var strict3 = Decoder.fromGuard((input) => typeof input === "boolean", `value is not a boolean`, { code: "bool.strict" /* BOOL_STRICT */ }); var fromString3 = pipe6(TextDecoder.string, Decoder.parse((str) => { const low = str.toLowerCase(); return TEXT_TRUE.has(low) ? Result2.ok(true) : TEXT_FALSE.has(low) ? Result2.ok(false) : Result2.ko(DecodeError.value(str, `string is not a boolean`)); }), Decoder.withMessage(`could not parse input string into a boolean`, { code: "bool.fromString" /* BOOL_FROM_STRING */ })); var fromNumber = pipe6(IntegerDecoder.strict, Decoder.parse((nb) => nb === 1 ? Result2.ok(true) : nb === 0 ? Result2.ok(false) : Result2.ko(DecodeError.value(nb, `number is not a boolean`))), Decoder.withMessage(`could not parse input number into a boolean`, { code: "bool.fromNumber" /* BOOL_FROM_NUMBER */ })); var boolean = pipe6(Decoder.union(strict3, fromString3, fromNumber), Decoder.withMessage(`value is not a boolean`, { code: "bool" /* BOOL */ })); var equals2 = (bool) => pipe6(boolean, Decoder.filter((value2) => value2 === bool, `boolean is not ${bool}`, { code: "bool.equals" /* BOOL_EQUALS */ })); var BooleanDecoder = { strict: strict3, boolean, equals: equals2, fromString: fromString3, fromNumber }; // src/ObjectDecoder.ts import { Dict as Dict3, Obj, pipe as pipe7, Result as Result3 } from "@apoyo/std"; var create2 = (props, decoder) => ({ props, ...decoder }); var unknownDict = Decoder.fromGuard((input) => typeof input === "object" && input !== null && !Array.isArray(input), `value is not an object`, { code: "dict" /* DICT */ }); var dict = (decoder) => { return pipe7(unknownDict, Decoder.parse((input) => pipe7(input, Result3.structBy((value2, key2) => pipe7(value2, Decoder.validate(decoder), Result3.mapError((err) => DecodeError.key(key2, err)))), Result3.mapError((errors) => DecodeError.object(errors))))); }; var struct = (props, name) => { return create2(props, pipe7(unknownDict, Decoder.parse((input) => pipe7(props, Result3.structBy((prop, key2) => pipe7(input[key2], Decoder.validate(prop), Result3.mapError((err) => DecodeError.key(key2, err)))), Result3.map(Dict3.compact), Result3.mapError((errors) => DecodeError.object(errors, name)))))); }; var guard2 = (fn) => (decoder) => create2(decoder.props, pipe7(decoder, Decoder.guard(fn))); function omit(props) { return (decoder) => pipe7(decoder.props, Obj.omit(props), struct); } function pick(props) { return (decoder) => pipe7(decoder.props, Obj.pick(props), struct); } function partial(decoder) { return pipe7(decoder.props, Dict3.map(Decoder.optional), struct); } function merge(...members) { return struct(Object.assign({}, ...members.map((m) => m.props))); } function sum(prop, cases) { const keys = Dict3.keys(cases); const typeDecoder = TextDecoder.oneOf(keys); return pipe7(unknownDict, Decoder.parse((input) => pipe7(input[prop], Decoder.validate(typeDecoder), Result3.chain((type) => pipe7(input, Decoder.validate(cases[type]), Result3.map((parsed) => ({ [prop]: type, ...parsed }))))))); } var additionalProperties = (decoder) => pipe7(unknownDict, Decoder.parse((input) => pipe7(input, Decoder.validate(decoder), Result3.map((parsed) => ({ ...input, ...parsed }))))); var ObjectDecoder = { unknownDict, dict, struct, omit, pick, partial, guard: guard2, merge, additionalProperties, sum }; // src/ArrayDecoder.ts import { Arr as Arr2, flow as flow3, pipe as pipe8, Result as Result4 } from "@apoyo/std"; var unknownArray = Decoder.fromGuard(Arr2.isArray, `value is not an array`, { code: "array" /* ARRAY */ }); var array2 = (decoder) => pipe8(unknownArray, Decoder.parse((input) => pipe8(input, Arr2.mapIndexed((value2, index2) => pipe8(value2, Decoder.validate(decoder), Result4.mapError((err) => DecodeError.index(index2, err)))), Arr2.separate, ([success, errors]) => errors.length > 0 ? Result4.ko(DecodeError.array(errors)) : Result4.ok(success)))); var nonEmptyArray = (decoder) => pipe8(array2(decoder), Decoder.filter(Arr2.isNonEmpty, `array should not be empty`, { code: "array.nonEmpty" /* ARRAY_NON_EMPTY */ })); var length2 = (len) => Decoder.filter((arr) => arr.length === len, `array should contain exactly ${len} elements`, { code: "array.length" /* ARRAY_LENGTH */, length: len }); var min4 = (minLength) => Decoder.filter((arr) => arr.length >= minLength, `array should contain at least ${minLength} elements`, { code: "array.min" /* ARRAY_MIN */, minLength }); var max4 = (maxLength) => Decoder.filter((arr) => arr.length <= maxLength, `array should contain at most ${maxLength} elements`, { code: "array.max" /* ARRAY_MAX */, maxLength }); var between4 = (minLength, maxLength) => flow3(min4(minLength), max4(maxLength)); var ArrayDecoder = { unknownArray, array: array2, nonEmptyArray, length: length2, min: min4, max: max4, between: between4 }; // src/DateDecoder.ts import { pipe as pipe9, Result as Result5 } from "@apoyo/std"; var isValid = Decoder.filter((date3) => !Number.isNaN(date3.getTime()), `string is not a valid Date`, { code: "date" /* DATE */ }); var createDateDecoder = (decoder) => { const fromString4 = pipe9(decoder, Decoder.map((str) => new Date(str))); return pipe9(Decoder.union(strict4, fromString4), isValid); }; var strict4 = Decoder.fromGuard((input) => input instanceof Date, `input is not a Date object`, { code: "date.strict" /* DATE_STRICT */ }); var date2 = createDateDecoder(TextDecoder.date); var datetime2 = createDateDecoder(TextDecoder.datetime); var native = createDateDecoder(TextDecoder.string); var min5 = (minDate) => Decoder.parse((input) => { const min6 = typeof minDate === "function" ? minDate() : minDate; return input.getTime() >= min6.getTime() ? Result5.ok(input) : Result5.ko(DecodeError.value(input, `date should be above ${min6.toISOString()}`, { code: "date.min" /* DATE_MIN */, min: min6 })); }); var max5 = (maxDate) => Decoder.parse((input) => { const max6 = typeof maxDate === "function" ? maxDate() : maxDate; return input.getTime() <= max6.getTime() ? Result5.ok(input) : Result5.ko(DecodeError.value(input, `date should be below ${max6.toISOString()}`, { code: "date.max" /* DATE_MAX */, max: max6 })); }); var DateDecoder = { date: date2, datetime: datetime2, strict: strict4, native, min: min5, max: max5 }; // src/EnumDecoder.ts import { Enum } from "@apoyo/std"; var create3 = (values, decoder) => ({ ...decoder, values }); var inSet = (set) => (value2) => set.has(value2); var native2 = (enumType) => { const values = Enum.values(enumType); const set = new Set(values); return create3(set, Decoder.fromGuard(inSet(set), `input does not match any value in enumeration`, { code: "enum.native" /* ENUM */, values })); }; var from = native2; var literal = (...values) => { const set = new Set(values); return create3(set, Decoder.fromGuard(inSet(set), `value is not equal to ${values.join(", or ")}`, { code: "enum.literal" /* ENUM_LITERAL */, values })); }; function isIn(arr) { const set = arr instanceof Set ? arr : new Set(arr); return create3(set, Decoder.fromGuard(inSet(set), `string is not included in the allowed list of values`, { code: "enum.isIn" /* ENUM_IS_IN */ })); } var EnumDecoder = { native: native2, from, literal, isIn }; export { ArrayDecoder, BooleanDecoder, DateDecoder, DecodeError, DecodeErrorTag, Decoder, EnumDecoder, ErrorCode, IntegerDecoder, NumberDecoder, ObjectDecoder, TextDecoder };