Skip to content

Feature Request: std.lib.validation — Schema & Data Validation Library Type: New Standard Library #20

Description

@bastitva0-blip

Feature Request: std.lib.validation — Schema & Data Validation Library

Type: New Standard Library
Area: Standard Library
Difficulty: Intermediate
Author: Bastitva
Related to: std/lib/ · CONTRIBUTING.md → "Standard Library: New built-in functions"


Summary

Add a validation.prox library to std/lib/ that provides schema-based data validation for ProXPL. It lets developers declare what shape their data should be, then check any value against that shape — getting back structured results with clear error messages, not just true/false.

This is distinct from everything that already exists:

  • regex.prox — does pattern matching on strings
  • crypto.prox — does hashing and encoding
  • str.prox — does string manipulation

None of them answer the question "does this dictionary / object / user input conform to a contract?". That gap is what this library fills.


Problem This Solves

ProXPL already has 75+ built-in functions and a growing standard library. But as soon as you write anything that accepts user input, reads a config file, parses a JSON API response, or receives data from a network call, you face the same problem — you don't know if the data is what you expect, and validating it by hand is repetitive, error-prone, and clutters your business logic.

What developers have to do today (without this library)

// Validating a user registration form — manual, verbose, fragile
func validateUser(data) {
    if (data["name"] == null) {
        print("Error: name is required");
        return false;
    }
    if (len(data["name"]) < 2) {
        print("Error: name must be at least 2 characters");
        return false;
    }
    if (data["age"] == null) {
        print("Error: age is required");
        return false;
    }
    if (data["age"] < 0 or data["age"] > 120) {
        print("Error: age must be between 0 and 120");
        return false;
    }
    if (!contains(data["email"], "@")) {
        print("Error: invalid email");
        return false;
    }
    // ... 20 more checks, growing forever
    return true;
}

Problems with the above:

  • Stops at the first error — the user sees one problem at a time
  • Mixes validation logic with application logic
  • Cannot be reused or composed across different parts of a program
  • No standard structure for error messages
  • Gets unmaintainable fast as data shapes grow

What developers can do with this library

use std.lib.validation;

const userSchema = Schema.define({
    name:     Validator.string().minLength(2).maxLength(50).required(),
    age:      Validator.number().min(0).max(120).required(),
    email:    Validator.string().email().required(),
    website:  Validator.string().url().optional(),
    tags:     Validator.list().items(Validator.string()).optional()
});

let result = userSchema.validate({
    name: "Alice",
    age: 25,
    email: "alice@example.com"
});

if (result.valid) {
    print("User is valid!");
} else {
    print("Validation failed:");
    for (let i = 0; i < len(result.errors); i = i + 1) {
        print("  - " + result.errors[i].field + ": " + result.errors[i].message);
    }
}

Clean, declarative, composable, and it collects all errors at once.


Proposed API

Core Classes

Validator — Chainable rule builder

The entry point. Every rule chain starts here. Each method returns the validator itself so rules can be chained.

// String validators
Validator.string()
    .required()             // field must be present and non-null
    .optional()             // field may be absent or null (default)
    .minLength(n)           // string length >= n
    .maxLength(n)           // string length <= n
    .length(n)              // string length == n exactly
    .pattern(str)           // must contain this substring/pattern
    .email()                // must look like an email address
    .url()                  // must start with http:// or https://
    .alphanumeric()         // only letters and digits
    .noWhitespace()         // no spaces allowed
    .oneOf(list)            // value must be one of the provided options
    .withMessage(msg)       // override the default error message

// Number validators
Validator.number()
    .required()
    .optional()
    .min(n)                 // value >= n
    .max(n)                 // value <= n
    .between(min, max)      // value >= min and <= max
    .integer()              // must be a whole number (no decimal part)
    .positive()             // must be > 0
    .negative()             // must be < 0
    .oneOf(list)
    .withMessage(msg)

// Boolean validators
Validator.bool()
    .required()
    .optional()
    .isTrue()               // must be exactly true
    .isFalse()              // must be exactly false
    .withMessage(msg)

// List validators
Validator.list()
    .required()
    .optional()
    .minItems(n)            // list must have at least n items
    .maxItems(n)            // list must have at most n items
    .items(validator)       // every item must pass this validator
    .withMessage(msg)

// Any-type validators
Validator.any()
    .required()
    .optional()
    .withMessage(msg)

Schema — Validates a full dictionary against a set of field rules

// Define a schema
const schema = Schema.define({
    fieldName: Validator.string().required(),
    otherField: Validator.number().optional()
});

// Validate data against it
let result = schema.validate(data);

// result structure:
// result.valid    → bool
// result.errors   → List of { field: string, message: string }
// result.data     → the original data (unchanged)

ValidationResult — Returned by every .validate() call

result.valid           // true if ALL rules passed
result.errors          // List of error objects: { field, message }
result.errorCount      // shortcut: len(result.errors)
result.hasError(field) // bool — does this specific field have an error?
result.getError(field) // string — first error message for a field
result.summary()       // string — human-readable summary of all errors

Validate — Standalone convenience functions (no schema needed)

For quick one-off checks without defining a full schema:

Validate.isEmail("test@example.com")       // bool
Validate.isURL("https://proxpl.org")       // bool
Validate.isNotEmpty("hello")               // bool
Validate.isInteger(42)                     // bool
Validate.isPositive(5)                     // bool
Validate.isInRange(value, min, max)        // bool
Validate.isOneOf(value, ["a", "b", "c"])   // bool
Validate.matchesLength(str, min, max)      // bool

Real-World Usage Examples

1. CLI Tool — Validating config file input

use std.lib.validation;
use std.lib.json;
use std.lib.fs;

const configSchema = Schema.define({
    host:        Validator.string().required().withMessage("host is required"),
    port:        Validator.number().min(1).max(65535).required(),
    debug:       Validator.bool().optional(),
    logLevel:    Validator.string().oneOf(["info", "warn", "error"]).optional(),
    maxRetries:  Validator.number().integer().positive().optional()
});

func loadConfig(path) {
    let raw = fs.readFile(path);
    let config = json.parse(raw);
    let result = configSchema.validate(config);

    if (!result.valid) {
        print("Invalid config file:");
        print(result.summary());
        return null;
    }

    return config;
}

// Output if port is missing and logLevel is wrong:
// Invalid config file:
//   - port: field is required
//   - logLevel: must be one of: info, warn, error

2. Web Server — Validating a request body

use std.lib.validation;

const registerSchema = Schema.define({
    username:  Validator.string().minLength(3).maxLength(20).alphanumeric().required(),
    password:  Validator.string().minLength(8).required(),
    email:     Validator.string().email().required(),
    age:       Validator.number().min(13).integer().required(),
    newsletter: Validator.bool().optional()
});

func handleRegister(body) {
    let result = registerSchema.validate(body);

    if (!result.valid) {
        return {
            status: 400,
            body: {
                success: false,
                errors: result.errors
            }
        };
    }

    // Proceed with registration — data is guaranteed clean
    createUser(body);
    return { status: 201, body: { success: true } };
}

3. Game Dev — Validating a save file or level config

use std.lib.validation;

const levelSchema = Schema.define({
    id:         Validator.number().integer().positive().required(),
    name:       Validator.string().minLength(1).maxLength(64).required(),
    difficulty: Validator.string().oneOf(["easy", "medium", "hard", "expert"]).required(),
    enemies:    Validator.list().minItems(1).maxItems(100).items(
                    Validator.string().oneOf(["goblin", "orc", "dragon"])
                ).required(),
    timeLimit:  Validator.number().positive().optional()
});

func loadLevel(data) {
    let result = levelSchema.validate(data);
    if (!result.valid) {
        print("Corrupt level data: " + result.summary());
        return null;
    }
    return data;
}

4. Nested / Composed Schemas

Schemas can validate individual fields, which enables layering validation for complex data:

use std.lib.validation;

// Validate the address part
const addressSchema = Schema.define({
    street: Validator.string().required(),
    city:   Validator.string().required(),
    zip:    Validator.string().length(6).alphanumeric().required()
});

// Validate the full order
const orderSchema = Schema.define({
    orderId:  Validator.string().required(),
    quantity: Validator.number().integer().min(1).max(999).required(),
    total:    Validator.number().positive().required()
});

func validateOrder(order) {
    let orderResult = orderSchema.validate(order);

    // Also validate the nested address field separately
    let addressResult = addressSchema.validate(order["shippingAddress"]);

    if (!orderResult.valid or !addressResult.valid) {
        print("Order validation failed:");
        if (!orderResult.valid)  print(orderResult.summary());
        if (!addressResult.valid) print("  (address) " + addressResult.summary());
        return false;
    }

    return true;
}

5. Standalone Validate functions for quick inline checks

use std.lib.validation;

let email = input("Enter your email: ");

if (!Validate.isEmail(email)) {
    print("That doesn't look like a valid email.");
} else {
    print("Email accepted!");
}

let age = to_int(input("Enter your age: "));
if (!Validate.isInRange(age, 0, 120)) {
    print("Age must be between 0 and 120.");
}

Why This Makes ProXPL Better

Fills a real gap without overlapping anything

ProXPL already has pattern matching (regex.prox), hashing (crypto.prox), and string utilities (str.prox). None of them do schema validation — checking that a dictionary or data structure matches a declared contract. This is a different job entirely.

Perfectly aligned with ProXPL's use cases

The README explicitly lists these intended use cases for the language:

  • High-performance web servers and network services — Every web handler needs input validation
  • Systems tooling and CLI applications — Config file validation is essential
  • Game scripting — Save/level data needs to be verified before use

This library makes ProXPL better at all three.

Fits the "batteries included" philosophy

The README's very first selling point is "Batteries Included: 75+ built-in standard library functions". A validation library is exactly the kind of foundational tool that a "batteries included" language should ship with. Languages like Python (pydantic), JavaScript (zod, joi), and Rust (validator) all have this — ProXPL should too.

ProXPL's type system makes this a natural fit

ProXPL already has static typing and compile-time type checking. This library extends that philosophy into runtime data — data that comes from outside the program (user input, files, network), where the compiler can't help. It's the same mindset, applied one layer out.

Chainable API matches the existing code style

Looking at regex.prox, crypto.prox, and collections.prox, the pattern is consistently ClassName.staticMethod() with method chaining. This library follows the exact same style — Validator.string().required().minLength(3) reads naturally alongside Math.clamp(), StringUtils.trim(), and Collections.map().


Implementation Plan

Files to create

std/lib/validation.prox          ← The library itself (pure ProXPL)
tests/test_validation.prox       ← Comprehensive test suite
docs/stdlib/validation.md        ← API documentation

Implementation approach

The library will be written entirely in ProXPL (like regex.prox, collections.prox, etc.) using existing built-in functions: len(), contains(), to_string(), to_number(), list_push(), substr(), type_of().

No changes to the C/C++ core are needed. No new native functions required.

Phased rollout

Phase 1 — Core validators and Schema (initial PR):

  • Validator.string() with all string rules
  • Validator.number() with all number rules
  • Validator.bool()
  • Schema.define() and .validate()
  • ValidationResult with .valid, .errors, .summary()

Phase 2 — List validation and standalone helpers (follow-up PR):

  • Validator.list().items()
  • Validator.any()
  • Validate.* convenience functions
  • Full documentation

Alternatives Considered

1. Extending regex.prox with validation helpers
Rejected — regex.prox is about pattern matching. Mixing schema validation into it would make both worse. Clean separation of concerns is better.

2. Using the existing assert() built-in
assert() throws on failure and stops execution. It's for tests and invariants, not for collecting user-facing errors and continuing. The use cases are fundamentally different.

3. Leaving it to userland / third-party packages
Possible, but validation is so universally needed that it belongs in the standard library. The "batteries included" philosophy means not making developers hunt for this on PRM for every new project.


Contribution Commitment

  • Write std/lib/validation.prox following CODING_STANDARD.md and K&R style
  • Write tests/test_validation.prox with integration tests covering all validator types
  • Write docs/stdlib/validation.md with full API reference and examples
  • Update std/README.md to list the new library
  • Follow Conventional Commits: feat(stdlib): add validation library
  • Open a draft PR early for feedback before finalizing

Questions for Maintainers

  1. Should the ValidationResult error list use ProXPL Dictionaries {field: "...", message: "..."} or a dedicated ValidationError class? (Class seems cleaner but adds overhead.)
  2. Is there a preferred way to check value types at runtime — should we use type_of() or rely on duck typing?
  3. Would you like Validator.list().items() to report per-index errors like tags[2]: must be a string, or just tags: invalid item?
  4. Any preference on the use import path — std.lib.validation or std.validation (shorter)?

Happy to discuss any of this in the comments or open a draft PR to start the conversation. Thanks for reviewing!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions