Skip to content

Commit

Permalink
Improve specification output for URL declarations
Browse files Browse the repository at this point in the history
  • Loading branch information
ezzatron committed Mar 16, 2024
1 parent dba2260 commit 47e27a8
Show file tree
Hide file tree
Showing 16 changed files with 112 additions and 35 deletions.
4 changes: 2 additions & 2 deletions src/declaration/big-integer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import { registerVariable } from "../environment.js";
import { Examples, create as createExamples } from "../example.js";
import { resolve } from "../maybe.js";
import { Scalar, createScalar, toString } from "../schema.js";
import { ScalarSchema, createScalar, toString } from "../schema.js";

export type Options = DeclarationOptions<bigint>;

Expand Down Expand Up @@ -37,7 +37,7 @@ export function bigInteger<O extends Options>(
};
}

function createSchema(): Scalar<bigint> {
function createSchema(): ScalarSchema<bigint> {
function unmarshal(v: string): bigint {
try {
return BigInt(v);
Expand Down
6 changes: 3 additions & 3 deletions src/declaration/binary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import { registerVariable } from "../environment.js";
import { Examples, create as createExamples } from "../example.js";
import { resolve } from "../maybe.js";
import { Scalar, createScalar } from "../schema.js";
import { ScalarSchema, createScalar } from "../schema.js";

const PATTERNS: Partial<Record<BufferEncoding, RegExp>> = {
base64: /^[A-Za-z0-9+/]*={0,2}$/,
Expand Down Expand Up @@ -45,7 +45,7 @@ export function binary<O extends Options>(
};
}

function createSchema(encoding: BufferEncoding): Scalar<Buffer> {
function createSchema(encoding: BufferEncoding): ScalarSchema<Buffer> {
function marshal(v: Buffer): string {
return v.toString(encoding);
}
Expand Down Expand Up @@ -76,7 +76,7 @@ function createUnmarshal(

function buildExamples(
encoding: BufferEncoding,
schema: Scalar<Buffer>,
schema: ScalarSchema<Buffer>,
): Examples {
return createExamples({
canonical: schema.marshal(Buffer.from("conquistador", "utf-8")),
Expand Down
4 changes: 2 additions & 2 deletions src/declaration/boolean.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import { registerVariable } from "../environment.js";
import { Examples, create as createExamples } from "../example.js";
import { resolve } from "../maybe.js";
import { Enum, InvalidEnumError, createEnum } from "../schema.js";
import { EnumSchema, InvalidEnumError, createEnum } from "../schema.js";
import { SpecError } from "../variable.js";

export type Options = DeclarationOptions<boolean> & {
Expand Down Expand Up @@ -47,7 +47,7 @@ export function boolean<O extends Options>(
};
}

function createSchema(name: string, literals: Literals): Enum<boolean> {
function createSchema(name: string, literals: Literals): EnumSchema<boolean> {
for (const literal of Object.keys(literals)) {
if (literal.length < 1) throw new EmptyLiteralError(name);
}
Expand Down
4 changes: 2 additions & 2 deletions src/declaration/duration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import { registerVariable } from "../environment.js";
import { Examples, create as createExamples } from "../example.js";
import { resolve } from "../maybe.js";
import { Scalar, createScalar, toString } from "../schema.js";
import { ScalarSchema, createScalar, toString } from "../schema.js";

const { Duration } = Temporal;
type Duration = Temporal.Duration;
Expand Down Expand Up @@ -41,7 +41,7 @@ export function duration<O extends Options>(
};
}

function createSchema(): Scalar<Duration> {
function createSchema(): ScalarSchema<Duration> {
function unmarshal(v: string): Duration {
try {
return Duration.from(v);
Expand Down
4 changes: 2 additions & 2 deletions src/declaration/enumeration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import { registerVariable } from "../environment.js";
import { Examples, create as createExamples } from "../example.js";
import { resolve } from "../maybe.js";
import { Enum, InvalidEnumError, createEnum } from "../schema.js";
import { EnumSchema, InvalidEnumError, createEnum } from "../schema.js";
import { SpecError } from "../variable.js";

export type Members<T> = Record<string, Member<T>>;
Expand Down Expand Up @@ -46,7 +46,7 @@ export function enumeration<T, O extends Options<T>>(
};
}

function createSchema<T>(name: string, members: Members<T>): Enum<T> {
function createSchema<T>(name: string, members: Members<T>): EnumSchema<T> {
const entries = Object.entries(members);

if (entries.length < 2) throw new InsufficientMembersError(name);
Expand Down
4 changes: 2 additions & 2 deletions src/declaration/integer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import { registerVariable } from "../environment.js";
import { Examples, create as createExamples } from "../example.js";
import { resolve } from "../maybe.js";
import { Scalar, createScalar, toString } from "../schema.js";
import { ScalarSchema, createScalar, toString } from "../schema.js";

export type Options = DeclarationOptions<number>;

Expand Down Expand Up @@ -37,7 +37,7 @@ export function integer<O extends Options>(
};
}

function createSchema(): Scalar<number> {
function createSchema(): ScalarSchema<number> {
function unmarshal(v: string): number {
const n = Number(v);

Expand Down
9 changes: 7 additions & 2 deletions src/declaration/kubernetes-address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import { registerVariable } from "../environment.js";
import { normalize } from "../error.js";
import { create as createExamples } from "../example.js";
import { Maybe, map, resolve } from "../maybe.js";
import { Scalar, createScalar, createString, toString } from "../schema.js";
import {
ScalarSchema,
createScalar,
createString,
toString,
} from "../schema.js";
import { Variable } from "../variable.js";

export type KubernetesAddress = {
Expand Down Expand Up @@ -144,7 +149,7 @@ function registerPort(
});
}

function createPortSchema(): Scalar<number> {
function createPortSchema(): ScalarSchema<number> {
function unmarshal(v: string): number {
if (!/^\d*$/.test(v)) throw new Error("must be an unsigned integer");
if (v !== "0" && v.startsWith("0")) {
Expand Down
4 changes: 2 additions & 2 deletions src/declaration/network-port-number.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import { registerVariable } from "../environment.js";
import { Examples, create as createExamples } from "../example.js";
import { resolve } from "../maybe.js";
import { Scalar, createScalar, toString } from "../schema.js";
import { ScalarSchema, createScalar, toString } from "../schema.js";

export type Options = DeclarationOptions<number>;

Expand Down Expand Up @@ -38,7 +38,7 @@ export function networkPortNumber<O extends Options>(
};
}

function createSchema(): Scalar<number> {
function createSchema(): ScalarSchema<number> {
function unmarshal(v: string): number {
if (!/^\d*$/.test(v)) throw new Error("must be an unsigned integer");
if (v !== "0" && v.startsWith("0")) {
Expand Down
4 changes: 2 additions & 2 deletions src/declaration/number.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import { registerVariable } from "../environment.js";
import { Examples, create as createExamples } from "../example.js";
import { resolve } from "../maybe.js";
import { Scalar, createScalar, toString } from "../schema.js";
import { ScalarSchema, createScalar, toString } from "../schema.js";

export type Options = DeclarationOptions<number>;

Expand Down Expand Up @@ -37,7 +37,7 @@ export function number<O extends Options>(
};
}

function createSchema(): Scalar<number> {
function createSchema(): ScalarSchema<number> {
function unmarshal(v: string): number {
const n = Number(v);

Expand Down
11 changes: 7 additions & 4 deletions src/declaration/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { registerVariable } from "../environment.js";
import { normalize } from "../error.js";
import { Example, Examples, create as createExamples } from "../example.js";
import { resolve } from "../maybe.js";
import { Scalar, createScalar, toString } from "../schema.js";
import { createURL, toString, type URLSchema } from "../schema.js";
import { Constraint, SpecError } from "../variable.js";

// as per https://www.rfc-editor.org/rfc/rfc3986#section-3.1
Expand All @@ -33,7 +33,7 @@ export function url<O extends Options>(
assertBase(name, validate, base);

const def = defaultFromOptions(options);
const schema = createSchema(base);
const schema = createSchema(base, protocols);

const v = registerVariable({
name,
Expand Down Expand Up @@ -89,7 +89,10 @@ function assertBase(
}
}

function createSchema(base: URL | undefined): Scalar<URL> {
function createSchema(
base: URL | undefined,
protocols: string[] | undefined,
): URLSchema {
function unmarshal(v: string): URL {
try {
const url = new URL(v, base);
Expand All @@ -100,7 +103,7 @@ function createSchema(base: URL | undefined): Scalar<URL> {
}
}

return createScalar("URL", toString, unmarshal);
return createURL(base, protocols, toString, unmarshal);
}

function createValidate(
Expand Down
38 changes: 31 additions & 7 deletions src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,28 @@ export type Schema<T> = {
export type MarshalFn<T> = Schema<T>["marshal"];
export type UnmarshalFn<T> = Schema<T>["unmarshal"];

export type Scalar<T> = Schema<T> & {
export type ScalarSchema<T> = Schema<T> & {
readonly description: string;
};

export type Enum<T> = Schema<T> & {
export type EnumSchema<T> = Schema<T> & {
readonly members: Record<string, T>;
};

export function createString(description: string): Scalar<string> {
export type URLSchema = Schema<URL> & {
readonly base: URL | undefined;
readonly protocols: string[] | undefined;
};

export function createString(description: string): ScalarSchema<string> {
return createScalar(description, identity, identity);
}

export function createEnum<T>(
members: Record<string, T>,
marshal: MarshalFn<T>,
unmarshal: UnmarshalFn<T>,
): Enum<T> {
): EnumSchema<T> {
return {
members,
marshal,
Expand All @@ -37,11 +42,29 @@ export function createEnum<T>(
};
}

export function createURL(
base: URL | undefined,
protocols: string[] | undefined,
marshal: MarshalFn<URL>,
unmarshal: UnmarshalFn<URL>,
): URLSchema {
return {
base,
protocols,
marshal,
unmarshal,

accept(visitor) {
return visitor.visitURL(this);
},
};
}

export function createScalar<T>(
description: string,
marshal: MarshalFn<T>,
unmarshal: UnmarshalFn<T>,
): Scalar<T> {
): ScalarSchema<T> {
return {
description,
marshal,
Expand All @@ -54,8 +77,9 @@ export function createScalar<T>(
}

export type Visitor<T> = {
visitEnum(e: Enum<unknown>): T;
visitScalar(s: Scalar<unknown>): T;
visitEnum(e: EnumSchema<unknown>): T;
visitScalar(s: ScalarSchema<unknown>): T;
visitURL(s: URLSchema): T;
};

export class InvalidEnumError<T> extends Error {
Expand Down
31 changes: 27 additions & 4 deletions src/specification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { Visitor } from "./schema.js";
import { quote } from "./shell.js";
import { Variable } from "./variable.js";

const disjunctionFormatter = new Intl.ListFormat("en", {
type: "disjunction",
});

export function render(variables: Variable<unknown>[]): string {
const app = appName();

Expand Down Expand Up @@ -85,21 +89,40 @@ function createSchemaRenderer(variable: Variable<unknown>): Visitor<string> {

return {
visitEnum({ members }) {
const listFormatter = new Intl.ListFormat("en", {
type: "disjunction",
});
const acceptableValues = Object.keys(members).map((m) =>
inlineCode(quote(m)),
);

return `The ${inlineCode(name)} variable is ${optionality} variable
that takes ${listFormatter.format(acceptableValues)}.`;
that takes ${disjunctionFormatter.format(acceptableValues)}.`;
},

visitScalar({ description }): string {
return `The ${inlineCode(name)} variable is ${optionality} variable
that takes ${strong(description)} values.`;
},

visitURL({ base, protocols = [] }): string {
const protocolList: string =
protocols.length > 0
? disjunctionFormatter.format(protocols.map((p) => inlineCode(p))) +
" "
: "";

const lines = [
`The ${inlineCode(name)} variable is ${optionality} variable
that takes ${strong(`${protocolList}URL`)} values.`,
];

if (base) {
lines.push(
`You can also use a URL reference relative to ` +
inlineCode(quote(base.toString())),
);
}

return lines.join("\n");
},
};
}

Expand Down
4 changes: 4 additions & 0 deletions src/summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ function createSchemaRenderer(): Visitor<string> {
visitScalar(s) {
return `<${s.description}>`;
},

visitURL() {
return "<URL>";
},
};
}

Expand Down
1 change: 1 addition & 0 deletions test/fixture/specification/url/base.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ _Main logo image_

The `LOGO` variable is a **required** variable
that takes **URL** values.
You can also use a URL reference relative to `https://base.example.org/path/to/resource`

### Example values

Expand Down
16 changes: 15 additions & 1 deletion test/fixture/specification/url/protocols.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,32 @@ The `<app>` app uses **declarative environment variables** powered by

| Name | Usage | Description |
| :-------------------------------- | :------- | :---------------------- |
| [`HOMEPAGE`](#HOMEPAGE) | Required | Main homepage URL |
| [`SOCKET_SERVER`](#SOCKET_SERVER) | Required | WebSocket server to use |

> [!TIP]
> If you set an empty value for an environment variable, the app behaves as if
> that variable isn't set.
## `HOMEPAGE`

_Main homepage URL_

The `HOMEPAGE` variable is a **required** variable
that takes **`https:` URL** values.

### Example values

```sh
export HOMEPAGE=https://host.example.org/path/to/resource # URL (https:)
```

## `SOCKET_SERVER`

_WebSocket server to use_

The `SOCKET_SERVER` variable is a **required** variable
that takes **URL** values.
that takes **`ws:` or `wss:` URL** values.

### Example values

Expand Down
Loading

0 comments on commit 47e27a8

Please sign in to comment.