Skip to content
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
218 changes: 9 additions & 209 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
A TypeScript validation library for Protobuf messages using [Spine Validation](https://github.com/SpineEventEngine/validation/) options,
built on [@bufbuild/protobuf](https://github.com/bufbuild/protobuf-es) (Protobuf-ES v2).

---
> **🔧 This library is in its experimental stage, the public API should not be considered stable.**

## 💡 Why Use This?

Expand All @@ -19,15 +19,15 @@ This library lets you:

### For New Users

Even if you're not using Spine Event Engine, this library provides a powerful way to add runtime validation to your Protobuf-based TypeScript applications:
Even if you're not using Spine Event Engine, this library provides a powerful way
to add runtime validation to your Protobuf-based TypeScript applications:

- ✅ **Define validation in `.proto` files** using declarative Spine validation options.
- ✅ **Define validation in `.proto` files** using declarative [Spine Validation options](https://github.com/SpineEventEngine/base-libraries/blob/master/base/src/main/proto/spine/options.proto).
- ✅ **Type-safe, runtime validation** for your Protobuf messages.
- ✅ **Clear, customizable error messages** for better UX.
- ✅ **Works with Protobuf-ES v2** and modern tooling.
- ✅ **Extensible architecture** for custom validation logic.

---

## ✨ Features

Expand All @@ -41,7 +41,7 @@ Even if you're not using Spine Event Engine, this library provides a powerful wa
- **`(validate)`** — Recursive nested message validation.
- **`(goes)`** — Field dependency constraints.
- **`(require)`** — Complex required field combinations with boolean logic.
- **`(choice)`** — Require that a oneof group has at least one field set.
- **`(choice)`** — Require that a `oneof` group has at least one field set.

**Developer Experience**

Expand All @@ -56,111 +56,17 @@ Even if you're not using Spine Event Engine, this library provides a powerful wa
- **`(set_once)`** — Not currently supported. This option requires state tracking across multiple validations,
which is outside the scope of single-message validation.

---

## 🚀 Quick Start

### Prerequisites

This library requires:
- **[Buf](https://buf.build/)** for Protobuf code generation
- **[@bufbuild/protobuf](https://github.com/bufbuild/protobuf-es)** (Protobuf-ES v2) for TypeScript/JavaScript runtime
## 🚀 Getting Started

Your TypeScript code must be generated using Buf's Protobuf-ES code generator (`@bufbuild/protoc-gen-es`).
This library is specifically designed to work with Buf-generated TypeScript code and will not work out-of-the-box
with other Protobuf code generators.
See the [package-level README](packages/spine-validation-ts/README.md) for complete installation instructions and usage guide.

### Installation

This package is currently published as a **pre-release (snapshot)** version.
Install it using the `@snapshot` dist-tag:
**Quick install:**

```bash
npm install @spine-event-engine/validation-ts@snapshot @bufbuild/protobuf
```

To install a specific snapshot version:

```bash
npm install @spine-event-engine/validation-ts@2.0.0-snapshot.3 @bufbuild/protobuf
```

> **Note:** This library is in active development and therefore it is published as a snapshot.

### Usage Guide

#### Step 1: Configure Buf for code generation

Create a `buf.gen.yaml` file in your project root:

```yaml
version: v2
plugins:
- remote: buf.build/protocolbuffers/es:v2.2.3
out: src/generated
```

#### Step 2: Define validation in your Proto files

Create your `.proto` file with Spine validation options:

```protobuf
syntax = "proto3";

import "spine/options.proto";

message User {
string name = 1 [(required) = true];

string email = 2 [
(required) = true,
(pattern).regex = "^[^@]+@[^@]+\\.[^@]+$",
(pattern).error_msg = "Email must be valid. Provided: `{value}`."
];

int32 age = 3 [
(min).value = "0",
(max).value = "150"
];

repeated string tags = 4 [(distinct) = true];
}
```

#### Step 3: Generate TypeScript code

Run Buf to generate TypeScript code from your proto files:

```bash
buf generate
```

This generates TypeScript schemas in `src/generated/` that include all validation metadata.

#### Step 4: Use Validation library in your TypeScript code

```typescript
import { create } from '@bufbuild/protobuf';
import { validate, Violations } from '@spine-event-engine/validation-ts';
import { UserSchema } from './generated/user_pb';

const user = create(UserSchema, {
name: '', // Missing required field
email: 'invalid-email' // Invalid pattern
});

const violations = validate(UserSchema, user);

if (violations.length > 0) {
violations.forEach(violation => {
const fieldPath = Violations.failurePath(violation);
const message = Violations.formatMessage(violation);

console.error(`${violation.typeName}.${fieldPath}: ${message}`);
});
}
```

---

## 📦 What's Included
Expand All @@ -184,13 +90,10 @@ validation-ts/
└── README.md # You are here
```

---

## 🎓 Documentation

- **[Package README](packages/spine-validation-ts/README.md)** - Complete API documentation and usage guide.
- **[Descriptor API Guide](packages/spine-validation-ts/DESCRIPTOR_API_GUIDE.md)** - Working with message and field descriptors.
- **[Quick Reference](packages/spine-validation-ts/QUICK_REFERENCE.md)** - Cheat sheet for common operations.
See the [package-level README](packages/spine-validation-ts/README.md) for more details.

---

Expand Down Expand Up @@ -230,108 +133,6 @@ npm run example

---

## 📋 Validation Options Reference

### Field-Level Options

| Option | Description | Example |
|--------|-------------|---------|
| `(required)` | Field must have a non-default value | `[(required) = true]` |
| `(if_missing)` | Custom error for missing field | `[(if_missing).error_msg = "Name is required"]` |
| `(pattern)` | Regex pattern matching | `[(pattern).regex = "^[A-Z].*"]` |
| `(min)` / `(max)` | Numeric minimum/maximum | `[(min).value = "0", (max).value = "100"]` |
| `(range)` | Bounded numeric range | `[(range) = "[0..100]"]` |
| `(distinct)` | Unique repeated elements | `[(distinct) = true]` |
| `(validate)` | Validate nested messages | `[(validate) = true]` |
| `(goes)` | Field dependency | `[(goes).with = "other_field"]` |

### Message-Level Options

| Option | Description | Example |
|--------|-------------|---------|
| `(require)` | Required field combinations | `option (require).fields = "id \| email";` |

### Oneof-Level Options

| Option | Description | Example |
|--------|-------------|---------|
| `(choice)` | Require oneof to have a field set | `option (choice).required = true;` |

### Not Supported

| Option | Status | Notes |
|--------|--------|-------|
| `(if_invalid)` | ❌ Not supported | Deprecated field-level option |
| `(set_once)` | ❌ Not supported | Requires state tracking across validations. See [limitations](#-known-limitations) |
| `(if_set_again)` | ❌ Not supported | Companion to `(set_once)` |
| `(is_required)` | ❌ Not supported | Deprecated. Use `(choice)` instead |
| `(required_field)` | ❌ Not supported | Deprecated. Use `(require)` instead |

---

## ✅ Test Coverage

The package includes comprehensive test coverage:

- **200+ tests** across 11 test suites
- **All validation options** thoroughly tested
- **Integration tests** combining multiple constraints
- **Edge cases** and real-world scenarios
- **100% coverage** of validation logic

Test suites:
- Basic validation
- Required fields
- Pattern matching
- Min/Max constraints
- Range validation
- Distinct elements
- Nested validation
- Field dependencies (`goes`)
- Required field combinations (`require`)
- `fneof` validation (`choice`)
- Integration scenarios

---

## 📝 Working with Violations

When validation fails, you can access detailed information from each violation:

```typescript
import { validate, Violations } from '@spine-event-engine/validation-ts';

const violations = validate(UserSchema, user);

violations.forEach(violation => {
// Use Violations utility object for easy access to violation details
const field = Violations.failurePath(violation);
const message = Violations.formatMessage(violation);

console.error(`${violation.typeName}.${field}: ${message}`);

// Example outputs:
// "User.name: A value must be set."
// "User.email: Email must be valid. Provided: `invalid-email`."
// "User.age: Value must be at least 0. Provided: -5."
// "User.tags: Values must be distinct. Duplicates found: [\"test\"]."
});
```

---

## 🏗️ Architecture

The validation system is built with extensibility in mind:

- **`validation.ts`** — Core validation engine using the visitor pattern
- **`options-registry.ts`** — Dynamic registration of validation options
- **`options/`** — Modular validators for each Spine option
- **Proto-first** — Validation rules defined in `.proto` files
- **Type-safe** — Full TypeScript support with generated types

---

## 🤝 Contributing

Contributions are welcome! Please ensure:
Expand All @@ -351,7 +152,6 @@ Apache 2.0.

## 🔗 Related Projects

- [Spine Event Engine](https://spine.io/) — Event-driven framework for CQRS/ES applications
- [Protobuf-ES](https://github.com/bufbuild/protobuf-es) — Protocol Buffers for ECMAScript
- [Buf](https://buf.build/) — Modern Protobuf tooling

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@spine-event-engine/validation-ts-workspace",
"version": "2.0.0-snapshot.3",
"version": "2.0.0-snapshot.4",
"private": true,
"workspaces": [
"packages/*"
Expand Down
2 changes: 1 addition & 1 deletion packages/example/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@spine-event-engine/example-smoke",
"version": "2.0.0-snapshot.3",
"version": "2.0.0-snapshot.4",
"private": true,
"description": "Example project demonstrating @spine-event-engine/validation-ts usage",
"type": "module",
Expand Down
Loading