npm package: @jaypie/vocabulary
Description: Jaypie standard application component vocabulary
Author: Finlayson Studio
Latest version: 0.2.0

# Jaypie Vocabulary ⛲️

## Philosophies

### Fabric

* Smooth, pliable
* Things that feel right should work
* Catch bad passes

## Concepts

* Fluid scalar coercion, string to object conversion
* Primitives: Boolean, String, Number (scalars); Objects, Arrays (considered different)
* Undefined always available
* "Resolvable" as scalar or function with potential promise
* Treat native types and their string representation equally (`type: String` or `type: "string"`)

### Coercion

#### Arrays

* Non-arrays become arrays of that value
* Arrays of a single value become that value
* Multi-value arrays throw bad request

#### Objects

* Scalars become { value: Boolean | Number | String }
* Arrays become { value: [] }
* Objects with a value attribute attempt coercion with that value
* Objects without a value throw bad request

#### Scalars

* String `""` becomes `undefined`
* String `"true"` becomes `true` or `1`
* String `"false"` becomes `false` or `0`
* Strings that parse to numbers use those numeric values to convert to number or boolean
* Strings that parse to `NaN` throw bad request
* Boolean `true` becomes "true" or `1`
* Boolean `false` becomes "false" or `0`
* Numbers to String are trivial
* Numbers convert to `true` when positive
* Numbers convert to `false` when zero or negative

### Entity Model

```
{
  "model": "job",
  "type": "command",
  "job": "evaluation",
  "plan": "hello"
}
```

### Service Handler

```typescript
import { serviceHandler } from "@jaypie/vocabulary";

const divisionHandler = serviceHandler({
  alias: "division",
  description: "Divides two numbers",
  input: {
    numerator: {
      default: 12,
      description: "Number 'on top', which is to be divided",
      type: Number,
    },
    denominator: {
      default: 3,
      description: "Number 'on bottom', how many ways to split the value",
      type: Number,
      validate: (value) => value !== 0,
    }
  },
  service: ({ numerator, denominator }) => (numerator / denominator),
});

await divisionHandler(); // =4
await divisionHandler({ numerator: 24 }); // =8
await divisionHandler({ numerator: 24, denominator: 2 }); // =12
await divisionHandler({ numerator: "14", denominator: "7" }); // =2
await divisionHandler({ numerator: 1, denominator: 0 }); // throws BadRequestError(); does not validate
await divisionHandler({ numerator: 1, denominator: "0" }); // throws BadRequestError(); does not validate
await divisionHandler('{ "numerator": "18" }'); // =3; String parses as JSON
await divisionHandler({ numerator: "ONE" }); // throws BadRequestError(); cannot coerce NaN to Number
await divisionHandler({ denominator: "TWO" }); // throws BadRequestError(); cannot coerce NaN to Number
await divisionHandler(12, 2); // throws BadRequestError(); future argument coercion may allow
```

Service Handler builds a function that initiates a "controller" step that:
* Parses the input if it is a string to object
* Coerces each input field to its type
* Calls the validation function or regular expression or checks the array
  * The validation function may return false or throw
  * A regular expression should be used as a matcher
  * An array should validate if any scalar matches when coerced to the same type, any regular expression matches, or any function returns true (pocket throws in this case)
* Calls the service function and returns the response (or returns the processed input if no service is provided)
* Parameters are assumed required unless (a) they have a default or (b) they are `required: false`

#### Handler Properties

The returned handler exposes config properties directly for introspection, useful for building CLI adapters and documentation:

```typescript
const handler = serviceHandler({
  alias: "division",
  description: "Divides two numbers",
  input: {
    numerator: { type: Number, default: 12 },
    denominator: { type: Number, default: 3 },
  },
  service: ({ numerator, denominator }) => numerator / denominator,
});

handler.alias;       // "division"
handler.description; // "Divides two numbers"
handler.input;       // { numerator: {...}, denominator: {...} }
```

#### Validation Only (No Service)

When no `service` function is provided, the handler returns the coerced and validated input:

```typescript
const validateUser = serviceHandler({
  input: {
    age: { type: Number, validate: (v) => v >= 18 },
    email: { type: [/^[^@]+@[^@]+\.[^@]+$/] },
    role: { default: "user", type: ["admin", "user", "guest"] },
  },
  // no service - returns processed input
});

await validateUser({ age: "25", email: "bob@example.com" });
// → { age: 25, email: "bob@example.com", role: "user" }

await validateUser({ age: 16, email: "teen@example.com" });
// throws BadRequestError - age validation fails
```

#### Natural Types

* `Array`
  * `[]` same as `Array`
  * `[Boolean]` same as `Array<boolean>`
  * `[Number]` same as `Array<number>`
  * `[Object]` same as `[{}]` same as `Array<object>`
  * `[String]` same as `[""]` same as `Array<string>`
* `Boolean`
* `Number`
* `Object`
  * `{}` same as `Object`
* `String`
  * `""` same as `String`

#### Typed Array Coercion

Typed arrays (`[String]`, `[Number]`, `[Boolean]`, `[Object]`) coerce each element to the specified type.

```typescript
coerce([1, 2, 3], [String])        // → ["1", "2", "3"]
coerce(["1", "2"], [Number])       // → [1, 2]
coerce([1, 0, -1], [Boolean])      // → [true, false, false]
coerce([1, "hello"], [Object])     // → [{ value: 1 }, { value: "hello" }]
```

**String Splitting**: Strings containing commas or tabs are automatically split into arrays.

```typescript
coerce("1,2,3", [Number])          // → [1, 2, 3]
coerce("a, b, c", [String])        // → ["a", "b", "c"] (whitespace trimmed)
coerce("true\tfalse", [Boolean])   // → [true, false]
```

Priority order:
1. JSON parsing: `"[1,2,3]"` → `[1, 2, 3]`
2. Comma splitting: `"1,2,3"` → `["1", "2", "3"]`
3. Tab splitting: `"1\t2\t3"` → `["1", "2", "3"]`
4. Single element wrap: `"42"` → `["42"]`

#### RegExp Type Shorthand

A bare RegExp as type coerces to String and validates against the pattern:

```typescript
const handler = serviceHandler({
  input: {
    email: { type: /^[^@]+@[^@]+\.[^@]+$/ },
    url: { type: /^https?:\/\/.+/ },
  },
  service: ({ email, url }) => ({ email, url }),
});

await handler({ email: "bob@example.com", url: "https://example.com" });  // ✓
await handler({ email: "invalid", url: "https://example.com" });          // ✗ BadRequestError
```

#### Validated Type Shorthand

Arrays of literals validate a value against allowed options.

**String validation** - array of strings and/or RegExp:

```typescript
const sendMoneyHandler = serviceHandler({
  input: {
    amount: { type: Number },
    currency: { type: ["dec", "sps"] },  // Must be "dec" or "sps"
    user: { type: String },
  },
  service: ({ amount, currency, user }) => ({ amount, currency, user }),
});

await sendMoneyHandler({ amount: 100, currency: "dec", user: "bob" });  // ✓
await sendMoneyHandler({ amount: 100, currency: "usd", user: "bob" });  // ✗ BadRequestError
```

**Number validation** - array of numbers:

```typescript
const taskHandler = serviceHandler({
  input: {
    priority: { type: [1, 2, 3, 4, 5] },  // Must be 1-5
    title: { type: String },
  },
  service: ({ priority, title }) => ({ priority, title }),
});

await taskHandler({ priority: 1, title: "Urgent" });   // ✓
await taskHandler({ priority: 10, title: "Invalid" }); // ✗ BadRequestError
```

**Mixed string and RegExp validation**:

```typescript
const handler = serviceHandler({
  input: {
    value: { type: [/^test-/, "special"] },  // Matches /^test-/ OR equals "special"
  },
  service: ({ value }) => value,
});

await handler({ value: "test-123" });  // ✓
await handler({ value: "special" });   // ✓
await handler({ value: "other" });     // ✗ BadRequestError
```

### Commander Adapter

The vocabulary package includes utilities for integrating service handlers with Commander.js CLIs.

#### registerServiceCommand

The simplest way to register a service handler as a Commander command:

```typescript
import { Command } from "commander";
import { serviceHandler } from "@jaypie/vocabulary";
import { registerServiceCommand } from "@jaypie/vocabulary/commander";

const handler = serviceHandler({
  alias: "greet",
  description: "Greet a user",
  input: {
    userName: { type: String, flag: "user", letter: "u" },
    loud: { type: Boolean, letter: "l", default: false },
  },
  service: ({ loud, userName }) => {
    const greeting = `Hello, ${userName}!`;
    return loud ? greeting.toUpperCase() : greeting;
  },
});

const program = new Command();
registerServiceCommand({ handler, program });
program.parse();
// Usage: greet --user Alice -l
```

Configuration options:
- `handler` - The service handler to register
- `program` - The Commander program or command
- `name` - Override command name (defaults to handler.alias)
- `description` - Override description (defaults to handler.description)
- `exclude` - Field names to exclude from options
- `overrides` - Per-field option overrides

#### Input Flag and Letter Properties

Input definitions support `flag` and `letter` for Commander.js integration:

```typescript
input: {
  userName: {
    type: String,
    flag: "user",     // Long flag: --user (instead of --user-name)
    letter: "u",      // Short flag: -u
  },
  verbose: {
    type: Boolean,
    letter: "v",      // Short flag: -v
  },
}
// Generates: --user <userName>, -u and --verbose, -v
```

#### createCommanderOptions

Generates Commander.js `Option` objects from handler input definitions:

```typescript
import { createCommanderOptions } from "@jaypie/vocabulary/commander";

const { options } = createCommanderOptions(handler.input, {
  exclude: ["internalField"],      // Fields to skip
  overrides: {
    userName: {
      short: "u",                   // Add short flag: -u
      description: "Override desc", // Override description
      hidden: true,                 // Hide from help
    },
  },
});
options.forEach((opt) => program.addOption(opt));
```

#### parseCommanderOptions

Converts Commander.js options back to handler input format with type coercion:

```typescript
import { parseCommanderOptions } from "@jaypie/vocabulary/commander";

const input = parseCommanderOptions(program.opts(), {
  input: handler.input,         // For type coercion and flag mapping
  exclude: ["help", "version"], // Fields to skip
});
```

#### Manual Integration Example

For more control, you can use createCommanderOptions and parseCommanderOptions directly:

```typescript
import { Command } from "commander";
import { serviceHandler } from "@jaypie/vocabulary";
import { createCommanderOptions, parseCommanderOptions } from "@jaypie/vocabulary/commander";

const handler = serviceHandler({
  input: {
    userName: { type: String, description: "User name" },
    maxRetries: { type: Number, default: 3, description: "Max retries" },
    verbose: { type: Boolean, description: "Verbose output" },
  },
  service: (input) => console.log(input),
});

const program = new Command();

// Create Commander options from handler input
const { options } = createCommanderOptions(handler.input);
options.forEach((opt) => program.addOption(opt));

program.action(async (opts) => {
  // Parse Commander options back to handler input
  const input = parseCommanderOptions(opts, { input: handler.input });
  await handler(input);
});

program.parse();
```

### Lambda Adapter

The vocabulary package includes utilities for integrating service handlers with AWS Lambda.

#### lambdaServiceHandler

Wraps a service handler for use as an AWS Lambda handler with full lifecycle management:

```typescript
import { serviceHandler } from "@jaypie/vocabulary";
import { lambdaServiceHandler } from "@jaypie/vocabulary/lambda";

const evaluationsHandler = serviceHandler({
  alias: "evaluationsHandler",
  input: {
    count: { type: Number, default: 1 },
    models: { type: [String], default: [] },
    plan: { type: String },
  },
  service: ({ count, models, plan }) => ({
    jobId: `job-${Date.now()}`,
    plan,
  }),
});

// Config object style
export const handler = lambdaServiceHandler({
  handler: evaluationsHandler,
  secrets: ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
});

// Or handler with options style
export const handler2 = lambdaServiceHandler(evaluationsHandler, {
  secrets: ["ANTHROPIC_API_KEY"],
  setup: [async () => { /* initialization */ }],
  teardown: [async () => { /* cleanup */ }],
});
```

Features:
- Uses `getMessages()` from `@jaypie/aws` to extract messages from SQS/SNS events
- Calls the service handler once for each message
- Returns single response if one message, array of responses if multiple
- Uses `handler.alias` as the logging handler name (overridable via `name` option)
- Supports all `lambdaHandler` options: `chaos`, `name`, `secrets`, `setup`, `teardown`, `throw`, `unavailable`, `validate`

#### Before/After Comparison

**Before** (manual lambdaHandler wrapping):
```typescript
export const handler = lambdaHandler(
  async (event: unknown) => {
    const messages = getMessages(event);
    const results = [];
    for (const message of messages) {
      const result = await evaluationsHandler(message);
      results.push({ status: "success", jobId: result.job.id, plan: message.plan });
    }
    return { status: "success", results };
  },
  {
    name: "evaluationsHandler",
    secrets: ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
  },
);
```

**After** (using lambdaServiceHandler):
```typescript
export const handler = lambdaServiceHandler({
  handler: evaluationsHandler,
  secrets: ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
});
```

### Serialization Formats

#### Complete Formats

Flat Model JSON
```json
{
  "model": "job",
  "type": "command",
  "job": "evaluation",
  "plan": "hello"
}
```
```json
{ errors: [] }
```

Response Model JSON
```json
{
  data: {
    "model": "entity",
    "id": "identifier"
  }
}
```
```json
{
  data: [
    { "model": "entity", "id": "identifier" },
    { "model": "entity", "id": "identifier" },
  ]
}
```
```json
{ 
  errors: [] 
}
```

#### Lookup Shorthand

JSON
```json
{ id: "identifier", model: "entity" }
```

String
```javascript
${model}-${id}
```

### Defined Vocabulary

#### Attributes

##### Defined Attributes

Should be treated as "strictly" defined:

* abbreviation: ""
* alias: ""
* createdAt: ""
* deletedAt: ""
* description: ""
* id: ""
* input: {}
* label: ""
* metadata: {}
* mock: {} | Boolean
* model: ""
* name: ""
* ou: ""
* output: {}
* state: {}
* type: ""
* updatedAt: ""
* xid: ""

##### Known Attributes

Ideally optimize for reusability:

* default: *
* input: {}
* service: ()
* value: ""
* validate: [()] | { "": () | // }

##### Assumed Attributes

Future intentions planned:

* authentication: () | [()]
* authorization: () | [()]
* chaos: ""
* class: ""
* context: {}
* controller: () => input | { input, context }
* env: ""
* history: {}
* locals: {}
* message: () | ""
* parameters: {}
* required: []
* role: ""
* seed: ""
* serializer: ()
* setup: [()]
* tags: [""]
* teardown: [()]

##### Avoidable Attributes

* body, data; prefer state
* subtype; prefer class

#### Models

* item
  * Not used directly

* record
  * types: text, json
  * attributes: content

* collection
  * types: models; e.g., records, jobs
  * attributes: items, subject

* job
  * types: api, call, command, event, schedule
  * attributes: job, plan, status

#### Extending Vocabulary

* The easiest way to extend the base vocabulary is with namespacing

## Style Guide

* Alphabetize anything