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 constrained_types package to the standard library #4493

Merged
merged 1 commit into from
Feb 13, 2024
Merged
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
9 changes: 9 additions & 0 deletions .release-notes/4493.md
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).
1 change: 1 addition & 0 deletions examples/constrained_type/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
constrained_type
35 changes: 35 additions & 0 deletions examples/constrained_type/README.md
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!
```
58 changes: 58 additions & 0 deletions examples/constrained_type/main.pony
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
124 changes: 124 additions & 0 deletions packages/constrained_types/_test.pony
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
69 changes: 69 additions & 0 deletions packages/constrained_types/constrained.pony
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
Loading
Loading