-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
84e8b56
commit 7c4d54b
Showing
13 changed files
with
422 additions
and
30 deletions.
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,99 @@ | ||
import { | ||
Cake, | ||
CakeDispatchCheckContext, | ||
CakeDispatchStringifyContext, | ||
CakeError, | ||
StringConstraints, | ||
StringConstraintsRefinement, | ||
StringRefinementCake, | ||
Refinement, | ||
WrongTypeCakeError, | ||
} from "./index-internal"; | ||
import type { | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
number, | ||
} from "./index-internal"; | ||
|
||
/** | ||
* See {@link string}. | ||
* | ||
* @public | ||
*/ | ||
class StringCake extends Cake<string> { | ||
/** | ||
* Only allow strings that satisfy the specified constraints. | ||
* | ||
* @example | ||
* | ||
* ```ts | ||
* const NonEmptyString = string.satisfying({ length: { min: 1 } }); | ||
* | ||
* NonEmptyString.as("hello"); // "hello" | ||
* | ||
* NonEmptyString.as(""); | ||
* // TypeError: String length is invalid: Number is less than the minimum of 1. | ||
* ``` | ||
* | ||
* Here, the length constraint is an object accepted by | ||
* {@link number.satisfying}; it can also be a number indicating the exact | ||
* length, or a Cake. | ||
* | ||
* @example Strings matching a regular expression (use `^` and `$` to match | ||
* the entire string): | ||
* | ||
* ```ts | ||
* const HexString = string.satisfying({ regex: /^[0-9a-f]+$/ }); | ||
* | ||
* HexString.as("123abc"); // "123abc" | ||
* | ||
* HexString.as("oops"); | ||
* // TypeError: String does not match regex /^[0-9a-f]+$/. | ||
* ``` | ||
*/ | ||
satisfying( | ||
constraints: StringConstraints | ||
): StringRefinementCake<string, string, this, StringConstraintsRefinement> { | ||
return this.refined(new StringConstraintsRefinement({ constraints })); | ||
} | ||
|
||
override refined<O extends string, R extends Refinement<string, O>>( | ||
refinement: R | ||
): StringRefinementCake<string, O, this, R> { | ||
return new StringRefinementCake({ base: this, refinement }); | ||
} | ||
|
||
dispatchCheck( | ||
value: unknown, | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
context: CakeDispatchCheckContext | ||
): CakeError | null { | ||
if (typeof value !== "string") { | ||
return new WrongTypeCakeError(this, value); | ||
} | ||
return null; | ||
} | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
dispatchStringify(context: CakeDispatchStringifyContext): string { | ||
return "string"; | ||
} | ||
|
||
withName(name: string | null): StringCake { | ||
return new StringCake({ ...this, name }); | ||
} | ||
} | ||
|
||
/** | ||
* A {@link Cake} representing the `string` type. | ||
* | ||
* @example | ||
* ```ts | ||
* string.is("hello"); // true | ||
* string.is(""); // true | ||
* ``` | ||
* | ||
* @public | ||
*/ | ||
const string = new StringCake({}); | ||
|
||
export { StringCake, string }; |
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,110 @@ | ||
import { | ||
bake, | ||
Cake, | ||
CakeError, | ||
CakeErrorDispatchFormatContext, | ||
NumberConstraints, | ||
NumberConstraintsRefinement, | ||
prependStringTree, | ||
Refinement, | ||
StringTree, | ||
} from "./index-internal"; | ||
|
||
/** | ||
* @public | ||
*/ | ||
interface StringConstraints { | ||
length?: number | NumberConstraints | Cake; | ||
regex?: RegExp; | ||
} | ||
|
||
/** | ||
* @public | ||
*/ | ||
interface StringConstraintsRefinementArgs { | ||
constraints: StringConstraints; | ||
} | ||
|
||
/** | ||
* @public | ||
*/ | ||
class StringConstraintsRefinement | ||
extends Refinement<string> | ||
implements StringConstraintsRefinementArgs | ||
{ | ||
readonly constraints: StringConstraints; | ||
private readonly lengthSpec: NumberConstraintsRefinement | Cake | null; | ||
|
||
constructor(args: StringConstraintsRefinementArgs) { | ||
super(); | ||
this.constraints = args.constraints; | ||
|
||
const rawLengthSpec = args.constraints.length; | ||
if (rawLengthSpec === undefined) { | ||
this.lengthSpec = null; | ||
} else if (typeof rawLengthSpec === "number") { | ||
this.lengthSpec = bake(rawLengthSpec); | ||
} else if (rawLengthSpec instanceof Cake) { | ||
this.lengthSpec = rawLengthSpec; | ||
} else { | ||
this.lengthSpec = new NumberConstraintsRefinement({ | ||
constraints: rawLengthSpec, | ||
}); | ||
} | ||
} | ||
|
||
dispatchCheck(value: string): CakeError | null { | ||
if (this.lengthSpec !== null) { | ||
const result = this.lengthSpec.check(value.length); | ||
if (!result.ok) { | ||
return new StringLengthCakeError(value, result.error); | ||
} | ||
} | ||
|
||
const regex = this.constraints.regex; | ||
if (regex !== undefined && !regex.test(value)) { | ||
return new RegexNotMatchedCakeError(value, regex); | ||
} | ||
|
||
return null; | ||
} | ||
|
||
toString(): string { | ||
const parts: string[] = []; | ||
if (this.lengthSpec !== null) { | ||
parts.push(`length ${this.lengthSpec}`); | ||
} | ||
if (this.constraints.regex !== undefined) { | ||
parts.push(`regex ${this.constraints.regex}`); | ||
} | ||
return parts.join(", "); | ||
} | ||
} | ||
|
||
class StringLengthCakeError extends CakeError { | ||
constructor(readonly value: string, readonly error: CakeError) { | ||
super(); | ||
} | ||
|
||
dispatchFormat(context: CakeErrorDispatchFormatContext): StringTree { | ||
const { recurse } = context; | ||
return prependStringTree("String length is invalid: ", recurse(this.error)); | ||
} | ||
} | ||
|
||
class RegexNotMatchedCakeError extends CakeError { | ||
constructor(readonly value: string, readonly regex: RegExp) { | ||
super(); | ||
} | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
dispatchFormat(context: CakeErrorDispatchFormatContext): StringTree { | ||
return `String does not match regex ${this.regex}.`; | ||
} | ||
} | ||
|
||
export { | ||
StringConstraints, | ||
StringConstraintsRefinement, | ||
StringConstraintsRefinementArgs, | ||
}; |
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,42 @@ | ||
import { | ||
Cake, | ||
StringConstraints, | ||
StringConstraintsRefinement, | ||
Refinement, | ||
RefinementCake, | ||
} from "./index-internal"; | ||
import type { | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
string, | ||
} from "./index-internal"; | ||
|
||
/** | ||
* @public | ||
*/ | ||
class StringRefinementCake< | ||
I extends string, | ||
O extends I, | ||
B extends Cake<I>, | ||
R extends Refinement<I, O> | ||
> extends RefinementCake<I, O, B, R> { | ||
/** | ||
* @see {@link string.satisfying}. | ||
*/ | ||
satisfying( | ||
constraints: StringConstraints | ||
): StringRefinementCake<O, O, this, StringConstraintsRefinement> { | ||
return this.refined(new StringConstraintsRefinement({ constraints })); | ||
} | ||
|
||
override refined<P extends O, R extends Refinement<O, P>>( | ||
refinement: R | ||
): StringRefinementCake<O, P, this, R> { | ||
return new StringRefinementCake({ base: this, refinement }); | ||
} | ||
|
||
override withName(name: string | null): StringRefinementCake<I, O, B, R> { | ||
return new StringRefinementCake({ ...this, name }); | ||
} | ||
} | ||
|
||
export { StringRefinementCake }; |
Oops, something went wrong.