oxidecomputer / oxidecomputer/typify

support for types with a fancy collection of constraints

Open
#456 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
898
Forks
114
Avg merge
4h 18m
Merged PRs (30d)
14

Description

Consider the current Name type from the Oxide API:

    {
      "title": "A name unique within the parent collection",
      "description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID though they may contain a UUID.",
      "type": "string",
      "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]$",
      "maxLength": 63
    }

This is fine, but parse errors are a little challenging for the user:

doesn't match pattern "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]*$" 

What if the type could help us by separating out each constraint and providing a description:

{
  "type": "string",
  "allOf": [
    {
      "title": "start",
      "description": "must start with a lower-case letter",
      "pattern": "^[a-z]"
    },
    {
      "title": "end",
      "description": "cannot end with a '-'",
      "not": {
        "pattern": "-$"
      }
    },
    {
      "title": "chars",
      "description": "must be composed of ASCII letters and numbers",
      "pattern": "^[a-zA-Z0-9-]*$"
    },
    {
      "title": "length",
      "description": "must be from 1 to 63 characters in length",
      "minLength": 1,
      "maxLength": 63
    },
    {
      "title": "uuid",
      "description": "must not be interpretable as a UUID",
      "not": {
        "format": "uuid"
      }
    }
  ]
}

This could produce code like this:

pub struct Name(String);

impl std::str::FromStr for Name {
    type Err = String;
    fn from(value: String) -> Self {
        if !regress::Regex::new("^[a-z]").unwrap().find(value).is_none() {
            return Err(format!(
                "could not convert {}: must start with a lower-case letter",
                value,
            ));
        }

        // ...

        if !uuid::Uuid::from_str(&value).is_err() {
            return Err(format!(
                "could not convert {}: must not be interpretable as a UUID",
                value,
            ));
        }

        Ok(Self(value.to_string()))
    }
}

This raises two questions:

  1. Is it a bunch of work?
  2. Would it conflict with schemas out there that weren't trying to opt into this type of behavior?

First on 2. It seems unlikely. Even for strings with a allOf constructions with a bunch of constraints, the worst we'd do is emit errors with descriptions not intended for that context... but you have to figure a description above a constraint might reasonably be expected to describe that constraint. What about constraints without description? We could generate a description: "didn't match the regex xyz", "exceeded the limit of xyz characters", etc.

On 1. Maybe, but it's just work. Right now, allOf constructions are pretty aggressive about merging, but I'm not sure what they'd do for string constraints. We could just be less aggressive about merging them for strings, leaving strings as "type + allOf array of constraints". Then the internal TypeEntryNewtype which currently allows for a solitary string constraint (but arrays of permitted or denied values) could grow an array of string constraints with optional descriptions of the constraints.

The hardest bit of representation and codegen seems to be how we would handle constraints of the form "must validate against this named type" or "must not validate against that named type", but it seems tractable.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by tracing the current allOf merging behavior for string constraints and the internal TypeEntryNewtype representation mentioned in the issue. Check how generated Rust validation code handles patterns, lengths, formats, and named types. Done means string constraints can remain separately represented with descriptions and produce useful validation errors without breaking existing schemas.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.