forked from jquense/yup
-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: implement standard schema #1
Draft
logaretm
wants to merge
5
commits into
master
Choose a base branch
from
feat/implement-standard-schema
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
a626f95
chore: added standard schema spec
logaretm 91d8478
feat: implement standard schema at the base schema
logaretm 3bc402f
test: added tests to verify behavior and type integrity with standard…
logaretm d857686
fix: split paths properly and add path tests
logaretm ea0e79f
chore: upgrade standard schema dep
logaretm File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
/** | ||
* Copied from @standard-schema/spec to avoid having a dependency on it. | ||
* https://github.com/standard-schema/standard-schema/blob/main/packages/spec/src/index.ts | ||
*/ | ||
|
||
import type { AnySchema } from './types'; | ||
import ValidationError from './ValidationError'; | ||
|
||
export interface StandardSchema<Input = unknown, Output = Input> { | ||
readonly '~standard': StandardSchemaProps<Input, Output>; | ||
} | ||
|
||
export interface StandardSchemaProps<Input = unknown, Output = Input> { | ||
readonly version: 1; | ||
readonly vendor: string; | ||
readonly validate: ( | ||
value: unknown, | ||
) => StandardResult<Output> | Promise<StandardResult<Output>>; | ||
readonly types?: StandardTypes<Input, Output> | undefined; | ||
} | ||
|
||
type StandardResult<Output> = | ||
| StandardSuccessResult<Output> | ||
| StandardFailureResult; | ||
|
||
interface StandardSuccessResult<Output> { | ||
readonly value: Output; | ||
readonly issues?: undefined; | ||
} | ||
|
||
interface StandardFailureResult { | ||
readonly issues: ReadonlyArray<StandardIssue>; | ||
} | ||
|
||
interface StandardIssue { | ||
readonly message: string; | ||
readonly path?: ReadonlyArray<PropertyKey | StandardPathSegment> | undefined; | ||
} | ||
|
||
interface StandardPathSegment { | ||
readonly key: PropertyKey; | ||
} | ||
|
||
interface StandardTypes<Input, Output> { | ||
readonly input: Input; | ||
readonly output: Output; | ||
} | ||
|
||
export function createStandardSchemaProps<TIn, Output>( | ||
schema: AnySchema, | ||
): StandardSchemaProps<TIn, Output> { | ||
/** | ||
* Adapts the schema's validate method to the standard schema's validate method. | ||
*/ | ||
async function validate(value: unknown): Promise<StandardResult<Output>> { | ||
try { | ||
const result = await schema.validate(value, { | ||
abortEarly: false, | ||
}); | ||
|
||
return { | ||
value: result as Output, | ||
}; | ||
} catch (err) { | ||
if (err instanceof ValidationError) { | ||
return { | ||
issues: issuesFromValidationError(err), | ||
}; | ||
} | ||
|
||
throw err; | ||
} | ||
} | ||
|
||
return { | ||
version: 1, | ||
vendor: 'yup', | ||
validate, | ||
}; | ||
} | ||
|
||
function createStandardPath(path: string | undefined): StandardIssue['path'] { | ||
if (!path?.length) { | ||
return undefined; | ||
} | ||
|
||
// Array to store the final path segments | ||
const segments: string[] = []; | ||
// Buffer for building the current segment | ||
let currentSegment = ''; | ||
// Track if we're inside square brackets (array/property access) | ||
let inBrackets = false; | ||
// Track if we're inside quotes (for property names with special chars) | ||
let inQuotes = false; | ||
|
||
for (let i = 0; i < path.length; i++) { | ||
const char = path[i]; | ||
|
||
if (char === '[' && !inQuotes) { | ||
// When entering brackets, push any accumulated segment after splitting on dots | ||
if (currentSegment) { | ||
segments.push(...currentSegment.split('.').filter(Boolean)); | ||
currentSegment = ''; | ||
} | ||
inBrackets = true; | ||
continue; | ||
} | ||
|
||
if (char === ']' && !inQuotes) { | ||
if (currentSegment) { | ||
// Handle numeric indices (e.g. arr[0]) | ||
if (/^\d+$/.test(currentSegment)) { | ||
segments.push(currentSegment); | ||
} else { | ||
// Handle quoted property names (e.g. obj["foo.bar"]) | ||
segments.push(currentSegment.replace(/^"|"$/g, '')); | ||
} | ||
currentSegment = ''; | ||
} | ||
inBrackets = false; | ||
continue; | ||
} | ||
|
||
if (char === '"') { | ||
// Toggle quote state for handling quoted property names | ||
inQuotes = !inQuotes; | ||
continue; | ||
} | ||
|
||
if (char === '.' && !inBrackets && !inQuotes) { | ||
// On dots outside brackets/quotes, push current segment | ||
if (currentSegment) { | ||
segments.push(currentSegment); | ||
currentSegment = ''; | ||
} | ||
continue; | ||
} | ||
|
||
currentSegment += char; | ||
} | ||
|
||
// Push any remaining segment after splitting on dots | ||
if (currentSegment) { | ||
segments.push(...currentSegment.split('.').filter(Boolean)); | ||
} | ||
|
||
return segments; | ||
} | ||
|
||
function createStandardIssues( | ||
error: ValidationError, | ||
parentPath?: string, | ||
): StandardIssue[] { | ||
return error.errors.map( | ||
(err) => | ||
({ | ||
message: err, | ||
path: createStandardPath( | ||
parentPath ? `${parentPath}.${error.path}` : error.path, | ||
), | ||
} satisfies StandardIssue), | ||
); | ||
} | ||
|
||
function issuesFromValidationError( | ||
error: ValidationError, | ||
parentPath?: string, | ||
): StandardIssue[] { | ||
if (!error.inner?.length && error.errors.length) { | ||
return createStandardIssues(error, parentPath); | ||
} | ||
|
||
return error.inner.flatMap((err) => | ||
issuesFromValidationError( | ||
err, | ||
parentPath ? `${parentPath}.${error.path}` : error.path, | ||
), | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import { | ||
string, | ||
number, | ||
array, | ||
bool, | ||
object, | ||
date, | ||
mixed, | ||
tuple, | ||
} from '../src'; | ||
import type { StandardSchemaV1 } from '@standard-schema/spec'; | ||
|
||
function verifyStandardSchema<Input, Output>( | ||
schema: StandardSchemaV1<Input, Output>, | ||
) { | ||
return ( | ||
schema['~standard'].version === 1 && | ||
schema['~standard'].vendor === 'yup' && | ||
typeof schema['~standard'].validate === 'function' | ||
); | ||
} | ||
|
||
test('is compatible with standard schema', () => { | ||
expect(verifyStandardSchema(string())).toBe(true); | ||
expect(verifyStandardSchema(number())).toBe(true); | ||
expect(verifyStandardSchema(array())).toBe(true); | ||
expect(verifyStandardSchema(bool())).toBe(true); | ||
expect(verifyStandardSchema(object())).toBe(true); | ||
expect(verifyStandardSchema(date())).toBe(true); | ||
expect(verifyStandardSchema(mixed())).toBe(true); | ||
expect(verifyStandardSchema(tuple([mixed()]))).toBe(true); | ||
}); | ||
|
||
test('issues path is an array of property paths', async () => { | ||
const schema = object({ | ||
obj: object({ | ||
foo: string().required(), | ||
'not.obj.nested': string().required(), | ||
}).required(), | ||
arr: array( | ||
object({ | ||
foo: string().required(), | ||
'not.array.nested': string().required(), | ||
}), | ||
).required(), | ||
'not.a.field': string().required(), | ||
}); | ||
|
||
const result = await schema['~standard'].validate({ | ||
obj: { foo: '', 'not.obj.nested': '' }, | ||
arr: [{ foo: '', 'not.array.nested': '' }], | ||
}); | ||
|
||
expect(result.issues).toEqual([ | ||
{ path: ['obj', 'foo'], message: 'obj.foo is a required field' }, | ||
{ | ||
path: ['obj', 'not.obj.nested'], | ||
message: 'obj["not.obj.nested"] is a required field', | ||
}, | ||
{ path: ['arr', '0', 'foo'], message: 'arr[0].foo is a required field' }, | ||
{ | ||
path: ['arr', '0', 'not.array.nested'], | ||
message: 'arr[0]["not.array.nested"] is a required field', | ||
}, | ||
{ path: ['not.a.field'], message: '["not.a.field"] is a required field' }, | ||
]); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this string here is not dependant on the map item
err
, so could it be declared outside of the.map
for a (very minor) perf improvement? Or should it actually depend on theerr
item somehow?(And the same goes for the
.flatMap
inissuesFromValidationError
)