-
-
Notifications
You must be signed in to change notification settings - Fork 415
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add constrained_types package to the standard library
Implements RFC 79. https://github.com/ponylang/rfcs/blob/main/text/0079-constrained-types.md Closes #4492
- Loading branch information
1 parent
0da5bd4
commit 2999c5f
Showing
8 changed files
with
445 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
## Add constrained types package to standard library | ||
|
||
We've added a new package to the standard library: `constrained_types`. | ||
|
||
The `constrained_types` package allows you to represent in the type system, domain rules like "Username must be 6 to 12 characters in length and only container lower case ASCII letters". | ||
|
||
To learn more about the package, checkout its [documentation on the standard library docs site](https://stdlib.ponylang.io/constrained_types--index/). | ||
|
||
You can learn more about the motivation behind the package by reading [the RFC](https://github.com/ponylang/rfcs/blob/main/text/0079-constrained-types.md). |
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 @@ | ||
constrained_type |
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,35 @@ | ||
# constrained_type | ||
|
||
Demonstrates the basics of using the `constrained_types` package to encode domain type constraints such as "number less than 10" in the Pony type system. | ||
|
||
The example program implements a type for usernames that requires that a username is between 6 and 12 characters long and only contains lower case ASCII characters. | ||
|
||
## How to Compile | ||
|
||
With a minimal Pony installation, in the same directory as this README file run `ponyc`. You should see content building the necessary packages, which ends with: | ||
|
||
```console | ||
... | ||
Generating | ||
Reachability | ||
Selector painting | ||
Data prototypes | ||
Data types | ||
Function prototypes | ||
Functions | ||
Descriptors | ||
Optimising | ||
Writing ./constrained_type.o | ||
Linking ./constrained_type | ||
``` | ||
|
||
## How to Run | ||
|
||
Once `constrained_type` has been compiled, in the same directory as this README file run `./constrained_type A_USERNAME`. Where `A_USERNAME` is a string you want to check to see if it meets the business rules above. | ||
|
||
For example, if you run `./constrained_type magenta` you should see: | ||
|
||
```console | ||
$ ./constrained_type magenta | ||
magenta is a valid username! | ||
``` |
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,58 @@ | ||
use "constrained_types" | ||
|
||
type Username is Constrained[String, UsernameValidator] | ||
type MakeUsername is MakeConstrained[String, UsernameValidator] | ||
|
||
primitive UsernameValidator is Validator[String] | ||
fun apply(string: String): ValidationResult => | ||
recover val | ||
let errors: Array[String] = Array[String]() | ||
|
||
if (string.size() < 6) or (string.size() > 12) then | ||
let msg = "Username must be between 6 and 12 characters" | ||
errors.push(msg) | ||
end | ||
|
||
for c in string.values() do | ||
if (c < 97) or (c > 122) then | ||
errors.push("Username can only contain lower case ASCII characters") | ||
break | ||
end | ||
end | ||
|
||
if errors.size() == 0 then | ||
ValidationSuccess | ||
else | ||
let failure = ValidationFailure | ||
for e in errors.values() do | ||
failure(e) | ||
end | ||
failure | ||
end | ||
end | ||
|
||
actor Main | ||
let _env: Env | ||
|
||
new create(env: Env) => | ||
_env = env | ||
|
||
try | ||
let arg1 = env.args(1)? | ||
let username = MakeUsername(arg1) | ||
match username | ||
| let u: Username => | ||
print_username(u) | ||
| let e: ValidationFailure => | ||
print_errors(e) | ||
end | ||
end | ||
|
||
fun print_username(username: Username) => | ||
_env.out.print(username() + " is a valid username!") | ||
|
||
fun print_errors(errors: ValidationFailure) => | ||
_env.err.print("Unable to create username") | ||
for s in errors.errors().values() do | ||
_env.err.print("\t- " + s) | ||
end |
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,124 @@ | ||
use "pony_test" | ||
|
||
actor \nodoc\ Main is TestList | ||
new create(env: Env) => PonyTest(env, this) | ||
new make() => None | ||
|
||
fun tag tests(test: PonyTest) => | ||
test(_TestFailureMultipleMessages) | ||
test(_TestFailurePlumbingWorks) | ||
test(_TestMultipleMessagesSanity) | ||
test(_TestSuccessPlumbingWorks) | ||
|
||
class \nodoc\ iso _TestSuccessPlumbingWorks is UnitTest | ||
""" | ||
Test that what should be a success, comes back as a success. | ||
We are expecting to get a constrained type back. | ||
""" | ||
fun name(): String => "constrained_types/SuccessPlumbingWorks" | ||
|
||
fun ref apply(h: TestHelper) => | ||
let less: USize = 9 | ||
|
||
match MakeConstrained[USize, _LessThan10Validator](less) | ||
| let s: Constrained[USize, _LessThan10Validator] => | ||
h.assert_true(true) | ||
| let f: ValidationFailure => | ||
h.assert_true(false) | ||
end | ||
|
||
class \nodoc\ iso _TestFailurePlumbingWorks is UnitTest | ||
""" | ||
Test that what should be a failure, comes back as a failure. | ||
This is a basic plumbing test. | ||
""" | ||
fun name(): String => "constrained_types/FailurePlumbingWorks" | ||
|
||
fun ref apply(h: TestHelper) => | ||
let more: USize = 11 | ||
|
||
match MakeConstrained[USize, _LessThan10Validator](more) | ||
| let s: Constrained[USize, _LessThan10Validator] => | ||
h.assert_true(false) | ||
| let f: ValidationFailure => | ||
h.assert_true(f.errors().size() == 1) | ||
h.assert_array_eq[String](["not less than 10"], f.errors()) | ||
end | ||
|
||
class \nodoc\ iso _TestMultipleMessagesSanity is UnitTest | ||
""" | ||
Sanity check that the _MultipleErrorsValidator works as expected and that | ||
we can trust the _TestFailureMultipleMessages working results. | ||
""" | ||
fun name(): String => "constrained_types/MultipleMessagesSanity" | ||
|
||
fun ref apply(h: TestHelper) => | ||
let string = "magenta" | ||
|
||
match MakeConstrained[String, _MultipleErrorsValidator](string) | ||
| let s: Constrained[String, _MultipleErrorsValidator] => | ||
h.assert_true(true) | ||
| let f: ValidationFailure => | ||
h.assert_true(false) | ||
end | ||
|
||
class \nodoc\ iso _TestFailureMultipleMessages is UnitTest | ||
""" | ||
Verify that collecting errors works as expected. | ||
""" | ||
fun name(): String => "constrained_types/FailureMultipleMessages" | ||
|
||
fun ref apply(h: TestHelper) => | ||
let string = "A1" | ||
|
||
match MakeConstrained[String, _MultipleErrorsValidator](string) | ||
| let s: Constrained[String, _MultipleErrorsValidator] => | ||
h.assert_true(false) | ||
| let f: ValidationFailure => | ||
h.assert_true(f.errors().size() == 2) | ||
h.assert_array_eq_unordered[String]( | ||
["bad length"; "bad character"], | ||
f.errors()) | ||
end | ||
|
||
primitive \nodoc\ _LessThan10Validator is Validator[USize] | ||
fun apply(num: USize): ValidationResult => | ||
recover val | ||
if num < 10 then | ||
ValidationSuccess | ||
else | ||
ValidationFailure("not less than 10") | ||
end | ||
end | ||
|
||
primitive \nodoc\ _MultipleErrorsValidator is Validator[String] | ||
fun apply(string: String): ValidationResult => | ||
recover val | ||
let errors: Array[String] = Array[String]() | ||
|
||
// Isn't too big or too small | ||
if (string.size() < 6) or (string.size() > 12) then | ||
let msg = "bad length" | ||
errors.push(msg) | ||
end | ||
|
||
// Every character is valid | ||
for c in string.values() do | ||
if (c < 97) or (c > 122) then | ||
errors.push("bad character") | ||
break | ||
end | ||
end | ||
|
||
if errors.size() == 0 then | ||
ValidationSuccess | ||
else | ||
// We have some errors, let's package them all up | ||
// and return the failure | ||
let failure = ValidationFailure | ||
for e in errors.values() do | ||
failure(e) | ||
end | ||
failure | ||
end | ||
end |
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,69 @@ | ||
type ValidationResult is (ValidationSuccess | ValidationFailure) | ||
|
||
primitive ValidationSuccess | ||
|
||
class val ValidationFailure | ||
""" | ||
Collection of validation errors. | ||
""" | ||
let _errors: Array[String val] = _errors.create() | ||
|
||
new create(e: (String val | None) = None) => | ||
match e | ||
| let s: String val => _errors.push(s) | ||
end | ||
|
||
fun ref apply(e: String val) => | ||
""" | ||
Add an error to the failure. | ||
""" | ||
_errors.push(e) | ||
|
||
fun errors(): this->Array[String val] => | ||
""" | ||
Get list of validation errors. | ||
""" | ||
_errors | ||
|
||
interface val Validator[T] | ||
""" | ||
Interface validators must implement. | ||
We strongly suggest you use a `primitive` for your `Validator` as validators | ||
are required to be stateless. | ||
""" | ||
new val create() | ||
fun apply(i: T): ValidationResult | ||
""" | ||
Takes an instance and returns either `ValidationSuccess` if it meets the | ||
constraint criteria or `ValidationFailure` if it doesn't. | ||
""" | ||
|
||
class val Constrained[T: Any val, F: Validator[T]] | ||
""" | ||
Wrapper class for a constrained type. | ||
""" | ||
let _value: T val | ||
|
||
new val _create(value: T val) => | ||
""" | ||
Private constructor that guarantees that `Constrained` can only be created | ||
by the `MakeConstrained` primitive in this package. | ||
""" | ||
_value = value | ||
|
||
fun val apply(): T val => | ||
""" | ||
Unwraps and allows access to the constrained object. | ||
""" | ||
_value | ||
|
||
primitive MakeConstrained[T: Any val, F: Validator[T] val] | ||
""" | ||
Builder of `Constrained` instances. | ||
""" | ||
fun apply(value: T): (Constrained[T, F] | ValidationFailure) => | ||
match F(value) | ||
| ValidationSuccess => Constrained[T, F]._create(value) | ||
| let e: ValidationFailure => e | ||
end |
Oops, something went wrong.