Context-sensitive validation / validation with parameters

interface Thing {
bars: string[];
foo: { baz: { bar: string }[]; }[];
}
interface Thing {
bars: string[];
foo: { baz: { bar: string }[]; }[];
}
What is the best/easiest way to write a validator which ensures that all bars are present in bars?
30 Replies
ssalbdivad
ssalbdivad2y ago
Probably a narrow? Even if we implemented the relative path comparison syntax, it would likely only be for equality checks like password: "string", "repeatPassword: "./password" or whatever. This is definitely too complex for a dedicated syntax.
Dimava
DimavaOP2y ago
I guess it would be possible with something like
const ThingBase = type({ bars: 'string[]' })
const Thing = ThingBase.morph((data, problems) => {
return scope({ bar: type.literal(...data.bars) }).type({
bars: 'bar[]';
foo: { baz: { bar: 'bar' }[]; }[];
})
})
const ThingBase = type({ bars: 'string[]' })
const Thing = ThingBase.morph((data, problems) => {
return scope({ bar: type.literal(...data.bars) }).type({
bars: 'bar[]';
foo: { baz: { bar: 'bar' }[]; }[];
})
})
or something , but would like to implement it like
const allowList = [];
const Thing = scope({
bar: narrow('string', s => allowList.includes(s))
}).narrow({ bars: 'string[]' }, (data) => {allowList = bars; return true})
.narrowToType({
bars: 'bar[]';
foo: { baz: { bar: 'bar' }[]; }[];
})
const allowList = [];
const Thing = scope({
bar: narrow('string', s => allowList.includes(s))
}).narrow({ bars: 'string[]' }, (data) => {allowList = bars; return true})
.narrowToType({
bars: 'bar[]';
foo: { baz: { bar: 'bar' }[]; }[];
})
So the thing I'm looking for is beforeValidate/afterValidate hooks to do smth like
const allowList = [];
const Thing = scope({
bar: narrow('string', s => allowList.includes(s))
}).type({
bars: 'bar[]';
foo: { baz: { bar: 'bar' }[]; }[];
}, {
beforeValidate: (data) => allowList = data.bars,
afterValidate: (data) => allowList = [],
})
const allowList = [];
const Thing = scope({
bar: narrow('string', s => allowList.includes(s))
}).type({
bars: 'bar[]';
foo: { baz: { bar: 'bar' }[]; }[];
}, {
beforeValidate: (data) => allowList = data.bars,
afterValidate: (data) => allowList = [],
})
ssalbdivad
ssalbdivad2y ago
Oh maybe you were asking for the opposite bars bars a bit confusing haha
Dimava
DimavaOP2y ago
Or even better, being able to use context like
const Thing = scope({
bar: narrow('string', (s, ctx) => ctx.allowList.includes(s))
}).type({
bars: 'bar[]';
foo: { baz: { bar: 'bar' }[]; }[];
}, {
beforeValidate: (data, ctx) => ctx.allowList = data.bars,
afterValidate: (data, ctx) => ctx.allowList = null,
})
const Thing = scope({
bar: narrow('string', (s, ctx) => ctx.allowList.includes(s))
}).type({
bars: 'bar[]';
foo: { baz: { bar: 'bar' }[]; }[];
}, {
beforeValidate: (data, ctx) => ctx.allowList = data.bars,
afterValidate: (data, ctx) => ctx.allowList = null,
})
ssalbdivad
ssalbdivad2y ago
That might be achievable at some point with the path syntax because it would be something like ../../bars[number]
Dimava
DimavaOP2y ago
I can't '..' because "I have no idea where the bar values are placed deep in the object"
ssalbdivad
ssalbdivad2y ago
Maybe /bars[number] would be from the root of the type
Dimava
DimavaOP2y ago
@ssalbdivad what is the most straightforward way to
const Chain =
type({ a: 'number' })
.narrow(type({ b: number }))
.narrow(type({ c: number }))
const Chain =
type({ a: 'number' })
.narrow(type({ b: number }))
.narrow(type({ c: number }))
(i.e. ordered validation) Or
const Chain = narrow('any', data => {
type({ a: number }).assert(data)
type({ b: number }).assert(data)
type({ c: number }).assert(data)
})
const Chain = narrow('any', data => {
type({ a: number }).assert(data)
type({ b: number }).assert(data)
type({ c: number }).assert(data)
})
or something
Unknown User
Unknown User2y ago
Message Not Public
Sign In & Join Server To View
ssalbdivad
ssalbdivad2y ago
What is the goal here? There's actually this related question I've been thinking about regarding preserving ordering through intersections of narrows. In general, narrows will run sequentially in the order they're defined, but they're not like morphs where the order itself is part of the type.
// as long as the narrows in l and r are individually safe to check // in the order they're specified, checking them in the order // resulting from this intersection should also be safe.
Dimava
DimavaOP2y ago
can you return Out from narrow, again?
ssalbdivad
ssalbdivad2y ago
No? Narrow is a constraint, not a morph If you mean can it be a type guard then yes, you use is like you normally would in TS
Dimava
DimavaOP2y ago
function withHooks<T extends Type<unknown>>(
type1: T,
before: (data: T['infer'], problems: Problems) => Out<T['infer']> | void,
after: (data: T['infer'], problems: Problems) => Out<T['infer']> | void,
): T {
return narrow('any', (data, problems) => {
const a = before(data, problems);
if (a.problems) { problems.inherit(a.problems); return false; }
const b = type1(data, problems);
if (b.problems) { problems.inherit(b.problems); return false; }
const c = after(data, problems);
if (c.problems) { problems.inherit(c.problems); return false; }
return true;
})
}
function withHooks<T extends Type<unknown>>(
type1: T,
before: (data: T['infer'], problems: Problems) => Out<T['infer']> | void,
after: (data: T['infer'], problems: Problems) => Out<T['infer']> | void,
): T {
return narrow('any', (data, problems) => {
const a = before(data, problems);
if (a.problems) { problems.inherit(a.problems); return false; }
const b = type1(data, problems);
if (b.problems) { problems.inherit(b.problems); return false; }
const c = after(data, problems);
if (c.problems) { problems.inherit(c.problems); return false; }
return true;
})
}
erm I dunno what I wrote
ssalbdivad
ssalbdivad2y ago
Couldn't you just use a morph?
Dimava
DimavaOP2y ago
...I wrote not the thing I did want I guess I'm not sure, I huess I can
ssalbdivad
ssalbdivad2y ago
Morphs are just arbitrary mappings, it makes sense to call other validators from them, and they encode sequentiality so that would probably be the natural way to do it
Dimava
DimavaOP2y ago
The thing is, I want to ... type().catch() Okay that does make proper sense
ssalbdivad
ssalbdivad2y ago
I think it is a good idea
Dimava
DimavaOP2y ago
Can you link me the playground? Is it pinned somewhere?
ssalbdivad
ssalbdivad2y ago
E.g. you want to transform some error to undefined for some config option you don't actually need to respect No it was just an idea, I have not followed up on it It would be somewhat complicated to represent in the type system unfortunately
Dimava
DimavaOP2y ago
nevermind its on https://arktype.io/docs/
Intro | ArkType
replace(./dev/arktype.io/static,) |> replace({ type },{"{"} type {"}"}) -->
ssalbdivad
ssalbdivad2y ago
I would have to think more about it. I guess it's just a morph at whatever level Oh sorry I thought you meant a playground with that functionality
Dimava
DimavaOP2y ago
So here's my test case
const bar = narrow("string", (d, p) => {
return true
})
const bs = scope({ bar })

const foo = bs.type({
bars: "bar[]",
checkedBar: "bar"
})

export const user = bs.type({ uncheckedBar: "bar", foo })

export const { data, problems } = user({
foo: {
bars: ["a"],
checkedBar: "b" // should error
},
uncheckedBar: "c" // should not error
})
const bar = narrow("string", (d, p) => {
return true
})
const bs = scope({ bar })

const foo = bs.type({
bars: "bar[]",
checkedBar: "bar"
})

export const user = bs.type({ uncheckedBar: "bar", foo })

export const { data, problems } = user({
foo: {
bars: ["a"],
checkedBar: "b" // should error
},
uncheckedBar: "c" // should not error
})
Dimava
DimavaOP2y ago
Dimava
DimavaOP2y ago
@ssalbdivad lemme explain I have a big-ass (somewhat legacy) graph-based object with, erm, 5 types of IDs and with depth of 5 or something
interface Model {
resources: {
resourceId: resourceId // ID source
metrics: metricId[] // this has to be checked to exist in model.metrics
links: {
resourceId: resourceId // this has to be checked to exist in model.resources
metricId: metricId // this has to be checked to exist in resource.metrics
}[]
}[]
metrics: {
metricId: metricId // ID source
}[]
}
interface Model {
resources: {
resourceId: resourceId // ID source
metrics: metricId[] // this has to be checked to exist in model.metrics
links: {
resourceId: resourceId // this has to be checked to exist in model.resources
metricId: metricId // this has to be checked to exist in resource.metrics
}[]
}[]
metrics: {
metricId: metricId // ID source
}[]
}
ssalbdivad
ssalbdivad2y ago
I just don't think anything other than a custom function is going to result in an efficient way of doing this
Dimava
DimavaOP2y ago
30 custom functions I think I'll try to make a morph-based wrapper for now
ssalbdivad
ssalbdivad2y ago
I think there are abstractions you could make around these kinds of scenarios for ID -based relationships and resolutions. Probably outside the scope of the core package, but could definitely be a useful library
Dimava
DimavaOP2y ago
GitHub
Add userDefinedContext to validation context · Issue #843 · arkty...
Request a feature Add userDefinedContext (along with objectPath and existing root and path) to validation context and userDefinedContext to define it 🤷 Motivation Sometimes you may want your childr...

Did you find this page helpful?