Skip to content
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

Add built in version coercion #43

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Added

- Added negated equality assertions for comparative and length. (#40)
- Added coercion for versions, which takes a number or array or string and turns it into a version (niave approach, may need some further work) (#11)

## Changed

Expand Down
6 changes: 5 additions & 1 deletion src/base-type.typ
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@
return (
valkyrie-type: true,
name: name,
description: if (description != none){ description } else { name },
description: if (description != none) {
description
} else {
name
},
optional: optional,
default: default,
types: types,
Expand Down
114 changes: 5 additions & 109 deletions src/coercions.typ
Original file line number Diff line number Diff line change
@@ -1,109 +1,5 @@

/// If the tested value is not already of dictionary type, the function provided as argument is expected to return a dictionary type with a shape that passes validation.
///
/// #example[```
/// #let schema = z.dictionary(
/// pre-transform: z.coerce.dictionary((it)=>(name: it)),
/// (name: z.string())
/// )
///
/// #z.parse("Hello", schema) \
/// #z.parse((name: "Hello"), schema)
/// ```]
///
/// - fn (function): Transformation function that the tested value and returns a dictionary that has a shape that passes validation.
#let dictionary(fn) = (self, it) => {
if (type(it) != type((:))) {
return fn(it)
}
it
}

/// If the tested value is not already of array type, it is transformed into an array of size 1
///
/// #example[```
/// #let schema = z.array(
/// pre-transform: z.coerce.array,
/// z.string()
/// )
///
/// #z.parse("Hello", schema) \
/// #z.parse(("Hello", "world"), schema)
/// ```]
#let array(self, it) = {
if (type(it) != type(())) {
return (it,)
}
it
}

/// Tested value is forceably converted to content type
///
/// #example[```
/// #let schema = z.content(
/// pre-transform: z.coerce.content
/// )
///
/// #type(z.parse("Hello", schema)) \
/// #type(z.parse(123456, schema))
/// ```]
#let content(self, it) = [#it]

/// An attempt is made to convert string, numeric, or dictionary inputs into datetime objects
///
/// #example[```
/// #let schema = z.date(
/// pre-transform: z.coerce.date
/// )
///
/// #z.parse(2020, schema) \
/// #z.parse("2020-03-15", schema) \
/// #z.parse("2020/03/15", schema) \
/// #z.parse((year: 2020, month: 3, day: 15), schema) \
/// ```]
#let date(self, it) = {
if (type(it) == type(datetime.today())) {
return it
}
if (type(it) == int) {
// assume this is the year
assert(
it > 1000 and it < 3000,
message: "The date is assumed to be a year between 1000 and 3000",
)
return datetime(year: it, month: 1, day: 1)
}

if (type(it) == str) {
let yearMatch = it.find(regex(`^([1|2])([0-9]{3})$`.text))
if (yearMatch != none) {
// This isn't awesome, but probably fine
return datetime(year: int(it), month: 1, day: 1)
}
let dateMatch = it.find(
regex(`^([1|2])([0-9]{3})([-\/])([0-9]{1,2})([-\/])([0-9]{1,2})$`.text),
)
if (dateMatch != none) {
let parts = it.split(regex("[-\/]"))
return datetime(
year: int(parts.at(0)),
month: int(parts.at(1)),
day: int(parts.at(2)),
)
}
panic("Unknown datetime object from string, try: `2020/03/15` as YYYY/MM/DD, also accepts `2020-03-15`")
}

if (type(it) == type((:))) {
if ("year" in it) {
return return datetime(
year: it.at("year"),
month: it.at("month", default: 1),
day: it.at("day", default: 1),
)
}
panic("Unknown datetime object from dictionary, try: `(year: 2022, month: 2, day: 3)`")
}
panic("Unknown date of type '" + type(it) + "' accepts: datetime, str, int, and object")

}
#import "coercions/dictionary.typ": dictionary
#import "coercions/array.typ": array
#import "coercions/content.typ": content
#import "coercions/date.typ": date
#import "coercions/version.typ": version
17 changes: 17 additions & 0 deletions src/coercions/array.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/// If the tested value is not already of array type, it is transformed into an array of size 1
///
/// #example[```
/// #let schema = z.array(
/// pre-transform: z.coerce.array,
/// z.string()
/// )
///
/// #z.parse("Hello", schema) \
/// #z.parse(("Hello", "world"), schema)
/// ```]
#let array(self, it) = {
if (type(it) != type(())) {
return (it,)
}
it
}
12 changes: 12 additions & 0 deletions src/coercions/content.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

/// Tested value is forceably converted to content type
///
/// #example[```
/// #let schema = z.content(
/// pre-transform: z.coerce.content
/// )
///
/// #type(z.parse("Hello", schema)) \
/// #type(z.parse(123456, schema))
/// ```]
#let content(self, it) = [#it]
58 changes: 58 additions & 0 deletions src/coercions/date.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/// An attempt is made to convert string, numeric, or dictionary inputs into datetime objects
///
/// #example[```
/// #let schema = z.date(
/// pre-transform: z.coerce.date
/// )
///
/// #z.parse(2020, schema) \
/// #z.parse("2020-03-15", schema) \
/// #z.parse("2020/03/15", schema) \
/// #z.parse((year: 2020, month: 3, day: 15), schema) \
/// ```]
#let date(self, it) = {
if (type(it) == datetime) {
return it
}
if (type(it) == int) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really unsure about this one, seems more surprising than useful to me.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was just a file being moved, definitely worth being looked at but I'm not sure if its for this PR (which deals with just the version coercion).

On the topic of datetime parsing, I think dictionaries should be supported, on ISO formats, ISO 8601? I wouldn't support offsets but other than that, that should be doable

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, dictionaries sound fine to me.

ISO 8601?

Yeah, the full year, full month and full day must be specified for a string to be a valid date, i.e. "07" or "30-5" are rejected, because they are ambiguous, "1970-01-01" or "1970-1-1" are valid. No further checks like checking for the max day of month, that's up to the impl of datetime and more complicated than necessary.

// assume this is the year
assert(
it > 1000 and it < 3000,
message: "The date is assumed to be a year between 1000 and 3000",
)
return datetime(year: it, month: 1, day: 1)
}

if (type(it) == str) {
let yearMatch = it.find(regex(`^([1|2])([0-9]{3})$`.text))
if (yearMatch != none) {
// This isn't awesome, but probably fine
return datetime(year: int(it), month: 1, day: 1)
}
let dateMatch = it.find(
regex(`^([1|2])([0-9]{3})([-\/])([0-9]{1,2})([-\/])([0-9]{1,2})$`.text),
)
if (dateMatch != none) {
let parts = it.split(regex("[-\/]"))
return datetime(
year: int(parts.at(0)),
month: int(parts.at(1)),
day: int(parts.at(2)),
)
}
panic("Unknown datetime object from string, try: `2020/03/15` as YYYY/MM/DD, also accepts `2020-03-15`")
}

if (type(it) == type((:))) {
if ("year" in it) {
return return datetime(
year: it.at("year"),
month: it.at("month", default: 1),
day: it.at("day", default: 1),
)
}
panic("Unknown datetime object from dictionary, try: `(year: 2022, month: 2, day: 3)`")
}
panic("Unknown date of type '" + type(it) + "' accepts: datetime, str, int, and object")

}
20 changes: 20 additions & 0 deletions src/coercions/dictionary.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

/// If the tested value is not already of dictionary type, the function provided as argument is expected to return a dictionary type with a shape that passes validation.
///
/// #example[```
/// #let schema = z.dictionary(
/// pre-transform: z.coerce.dictionary((it)=>(name: it)),
/// (name: z.string())
/// )
///
/// #z.parse("Hello", schema) \
/// #z.parse((name: "Hello"), schema)
/// ```]
///
/// - fn (function): Transformation function that the tested value and returns a dictionary that has a shape that passes validation.
#let dictionary(fn) = (self, it) => {
if (type(it) != type((:))) {
return fn(it)
}
it
}
25 changes: 25 additions & 0 deletions src/coercions/version.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#let stdversion = version

/// Tested value is forceably converted to version type
///
/// #example[```
/// #let schema = z.version(
/// pre-transform: z.coerce.version
/// )
///
/// #type(z.parse("0.1.1", schema)) \
/// #type(z.parse(1, schema))
/// #type(z.parse((1,1,), schema))
/// ```]
#let version(self, it) = {
if type(it) == str {
return stdversion(
it.split(".").filter(it => it != "").map(int),
)
} else if type(it) == int {
return stdversion(it)
} else if type(it) == array {
return stdversion(it.map(int))
}
it
}
2 changes: 1 addition & 1 deletion src/types/dictionary.typ
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
}
return it
},
..args.named()
..args.named(),
) + (
dictionary-schema: dictionary-schema,
handle-descendents: (self, it, ctx: z-ctx(), scope: ()) => {
Expand Down
5 changes: 4 additions & 1 deletion src/types/logical.typ
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@

base-type(
name: "either",
description: "[" + args.pos().map(it => it.name).join(", ", last: " or ") + "]",
description: "[" + args.pos().map(it => it.name).join(
", ",
last: " or ",
) + "]",
..args.named(),
) + (
strict: strict,
Expand Down
12 changes: 9 additions & 3 deletions src/types/tuple.typ
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,15 @@
tuple-exact: exact,
tuple-schema: args.pos(),
handle-descendents: (self, it, ctx: z-ctx(), scope: ()) => {
if (self.tuple-exact and self.tuple-schema.len() != it.len()){
(self.fail-validation)(self, it, ctx: ctx, scope: scope,
message: "Expected " + str(self.tuple-schema.len()) + " values, but got " + str(it.len())
if (self.tuple-exact and self.tuple-schema.len() != it.len()) {
(self.fail-validation)(
self,
it,
ctx: ctx,
scope: scope,
message: "Expected " + str(
self.tuple-schema.len(),
) + " values, but got " + str(it.len()),
)
}
for (key, schema) in self.tuple-schema.enumerate() {
Expand Down
Binary file added tests/coercions/version/ref/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions tests/coercions/version/test.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#import "/src/lib.typ" as z
#set page(height: auto, width: auto)

#let test(x) = z.parse(x, z.version(pre-transform: z.coerce.version))

#repr(test(1))

#repr(test((1, 1)))

#repr(test("1.1."))

#repr(test("1.0.1.0"))

4 changes: 2 additions & 2 deletions tests/types/tuple/test.typ
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
let test-tuple = (
"123",
"[email protected]",
1.1
)
1.1,
)

z.parse(
test-tuple,
Expand Down