Skip to content

Commit

Permalink
Add constrained_types package to the standard library
Browse files Browse the repository at this point in the history
  • Loading branch information
SeanTAllen committed Feb 7, 2024
1 parent 0da5bd4 commit 2999c5f
Show file tree
Hide file tree
Showing 8 changed files with 445 additions and 0 deletions.
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

0 comments on commit 2999c5f

Please sign in to comment.