@groton/canvas-api.swagger-renderer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,138 @@
1
+ import { Colors } from '@battis/qui-cli.colors';
2
+ import { Log } from '@battis/qui-cli.log';
3
+ import path from 'node:path';
4
+ let _overrides = {};
5
+ let _outputPath = undefined;
6
+ export function setOverrides(overrides) {
7
+ _overrides = overrides;
8
+ for (const tsReference of _overrides.tsReferences || []) {
9
+ if (tsReference.filePath) {
10
+ tsReference.filePath = path.resolve(outputPath(), tsReference.filePath);
11
+ }
12
+ }
13
+ for (const name in _overrides.tsTypes) {
14
+ for (const ref of _overrides.tsTypes[name].tsReferences || []) {
15
+ if (ref.filePath) {
16
+ ref.filePath = path.resolve(outputPath(), ref.filePath);
17
+ }
18
+ }
19
+ }
20
+ for (const nickname in _overrides.operations) {
21
+ for (const ref of _overrides.operations[nickname].tsType?.tsReferences ||
22
+ []) {
23
+ if (ref.filePath) {
24
+ ref.filePath = path.resolve(outputPath(), ref.filePath);
25
+ }
26
+ }
27
+ for (const tsImport of _overrides.operations[nickname].tsImports || []) {
28
+ if (tsImport.filePath) {
29
+ tsImport.filePath = path.resolve(outputPath(), tsImport.filePath);
30
+ }
31
+ }
32
+ for (const paramType of [
33
+ 'tsPathParameters',
34
+ 'tsQueryParameters',
35
+ 'tsFormParameters',
36
+ 'tsBodyParameters'
37
+ ]) {
38
+ // @ts-expect-error 7053 not gonna deal with _that_ type
39
+ for (const param of _overrides.operations[nickname][paramType] || []) {
40
+ if (param.tsType.tsReference?.filePath) {
41
+ param.tsType.tsReference.filePath = path.resolve(outputPath(), param.tsType.tsReference.filePath);
42
+ }
43
+ }
44
+ }
45
+ }
46
+ }
47
+ export function setOutputPath(outputPath) {
48
+ _outputPath = outputPath;
49
+ }
50
+ function outputPath() {
51
+ if (!_outputPath) {
52
+ throw new Error(`Overrides outputPath accessed before being initialized`);
53
+ }
54
+ return _outputPath;
55
+ }
56
+ export function tsReference(type) {
57
+ return _overrides.tsReferences?.find((ref) => ref.type === type);
58
+ }
59
+ export function tsType(type) {
60
+ let result = undefined;
61
+ if (_overrides.tsTypes && _overrides.tsTypes[type]) {
62
+ result = { description: type, ..._overrides.tsTypes[type] };
63
+ Log.debug(`Overriding ${Colors.value('type')} ${Colors.quotedValue(`"${type}"`)} as ${Colors.value(result.type)}`);
64
+ }
65
+ return result;
66
+ }
67
+ export function operation(operation) {
68
+ if (_overrides.operations && _overrides.operations[operation.nickname]) {
69
+ const override = _overrides.operations[operation.nickname];
70
+ Log.debug(`Overriding operation ${Colors.value(operation.nickname)} properties ${Object.getOwnPropertyNames(override)
71
+ .filter((p) => typeof p !== 'number')
72
+ .map(Colors.value)
73
+ .join(', ')}`);
74
+ const result = merge(operation, _overrides.operations[operation.nickname]);
75
+ for (const paramType of [
76
+ 'tsPathParameters',
77
+ 'tsQueryParameters',
78
+ 'tsFormParameters',
79
+ 'tsBodyParameters'
80
+ ]) {
81
+ if (paramType in result) {
82
+ // @ts-expect-error 7053 TODO paramType typing
83
+ result[paramType] = result[paramType].reduce((params, param) => {
84
+ const i = params.findIndex((p) => p.tsName === param.tsName);
85
+ if (i >= 0) {
86
+ params[i] = param;
87
+ }
88
+ else {
89
+ params.push(param);
90
+ }
91
+ return params;
92
+ }, []);
93
+ }
94
+ }
95
+ return result;
96
+ }
97
+ return operation;
98
+ }
99
+ function merge(a, b) {
100
+ if (a !== undefined && a !== null && b !== undefined && b !== null) {
101
+ if (Array.isArray(a)) {
102
+ if (Array.isArray(b)) {
103
+ return [...a, ...b];
104
+ }
105
+ else {
106
+ throw new TypeError(`Type mismatch, trying to merge ${typeof b} into an array.`);
107
+ }
108
+ }
109
+ else if (typeof a === 'object') {
110
+ if (typeof b === 'object') {
111
+ const result = {};
112
+ let key;
113
+ for (key of [
114
+ ...Array.from(new Set([
115
+ ...Object.getOwnPropertyNames(a),
116
+ ...Object.getOwnPropertyNames(b)
117
+ ]))
118
+ ]) {
119
+ // @ts-expect-error 2322 TODO setting previously unset property
120
+ result[key] = merge(a[key], b[key]);
121
+ }
122
+ return result;
123
+ }
124
+ else {
125
+ throw new TypeError(`Type mismatch, trying to merge ${typeof b} into an object.`);
126
+ }
127
+ }
128
+ else {
129
+ if (typeof a === typeof b) {
130
+ return b;
131
+ }
132
+ else {
133
+ throw new TypeError(`Type mismatch, trying to replace ${typeof a} with ${typeof b}`);
134
+ }
135
+ }
136
+ }
137
+ return (b || a);
138
+ }
@@ -0,0 +1,19 @@
1
+ import { PathString } from '@battis/descriptive-types';
2
+ import * as Plugin from '@battis/qui-cli.plugin';
3
+ type Configuration = Plugin.Configuration & {
4
+ specPath?: PathString;
5
+ overridePath?: PathString;
6
+ templatePath?: PathString;
7
+ outputPath?: PathString;
8
+ modelDirName?: string;
9
+ operationsDirName?: string;
10
+ prettierConfigPath?: PathString;
11
+ map?: boolean;
12
+ };
13
+ export declare const name = "render";
14
+ export declare const src: string;
15
+ export declare function configure(config?: Configuration): void;
16
+ export declare function options(): Plugin.Options;
17
+ export declare function init(args: Plugin.ExpectedArguments<typeof options>): void;
18
+ export declare function run(results?: Plugin.AccumulatedResults): Promise<void>;
19
+ export {};
@@ -0,0 +1,124 @@
1
+ import { Colors } from '@battis/qui-cli.colors';
2
+ import { Log } from '@battis/qui-cli.log';
3
+ import * as Plugin from '@battis/qui-cli.plugin';
4
+ import { Root } from '@battis/qui-cli.root';
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import ora from 'ora';
8
+ import * as Download from '../Download.js';
9
+ import * as Models from './Models.js';
10
+ import * as Operations from './Operations.js';
11
+ import * as Overrides from './Overrides.js';
12
+ Root.configure({ root: process.cwd() });
13
+ export const name = 'render';
14
+ export const src = path.dirname(import.meta.dirname);
15
+ let specPath = './spec';
16
+ let overridePath;
17
+ let templatePath = path.resolve(import.meta.dirname, '../../templates');
18
+ let outputPath = './src';
19
+ let modelDirName = 'Resources';
20
+ let operationsDirName = 'Endpoints';
21
+ let prettierConfigPath = './.prettierrc.json';
22
+ let map = false;
23
+ export function configure(config = {}) {
24
+ specPath = Plugin.hydrate(config.specPath, specPath);
25
+ overridePath = Plugin.hydrate(config.overridePath, overridePath);
26
+ templatePath = Plugin.hydrate(config.templatePath, templatePath);
27
+ outputPath = Plugin.hydrate(config.outputPath, outputPath);
28
+ modelDirName = Plugin.hydrate(config.modelDirName, modelDirName);
29
+ operationsDirName = Plugin.hydrate(config.operationsDirName, operationsDirName);
30
+ prettierConfigPath = Plugin.hydrate(config.prettierConfigPath, prettierConfigPath);
31
+ map = Plugin.hydrate(config.map, map);
32
+ }
33
+ export function options() {
34
+ return {
35
+ flag: {
36
+ map: {
37
+ description: `Output the annotated code map (default: ${Colors.value(map)})`,
38
+ default: map
39
+ }
40
+ },
41
+ opt: {
42
+ specPath: {
43
+ description: `Path to Swagger spec file or directory (default: ${Colors.url(specPath)})`,
44
+ default: specPath
45
+ },
46
+ overridePath: {
47
+ description: `Path to TypeScript types override JSON file (default: ${Colors.url(overridePath)})`,
48
+ default: overridePath
49
+ },
50
+ templatePath: {
51
+ description: `Path to Handlebars template directory (default: ${Colors.url(templatePath)})`,
52
+ default: templatePath
53
+ },
54
+ outputPath: {
55
+ description: `Path to output directory (default: ${Colors.url(outputPath)})`,
56
+ default: outputPath
57
+ },
58
+ modelDirName: {
59
+ description: `Name of resource definitions directory (default: ${Colors.quotedValue(`"${modelDirName}"`)})`,
60
+ default: modelDirName
61
+ },
62
+ operationsDirName: {
63
+ description: `Name of endpoint definitions directory (default: ${Colors.quotedValue(`"${operationsDirName}"`)})`,
64
+ default: operationsDirName
65
+ }
66
+ }
67
+ };
68
+ }
69
+ export function init(args) {
70
+ configure(args.values);
71
+ }
72
+ export async function run(results) {
73
+ const spinner = ora(`Looking for specs`).start();
74
+ let specPaths = undefined;
75
+ specPath = path.resolve(Root.path(), specPath);
76
+ if (results && results[Download.name]) {
77
+ specPaths = results[Download.name];
78
+ spinner.text = `Using specs provided by download`;
79
+ }
80
+ else if (fs.lstatSync(specPath).isDirectory()) {
81
+ specPaths = fs
82
+ .readdirSync(specPath)
83
+ .filter((fileName) => !fileName.startsWith('.'))
84
+ .map((fileName) => path.join(specPath, fileName));
85
+ spinner.text = `Using specs found in directory ${Colors.url(specPath)}`;
86
+ }
87
+ else {
88
+ specPaths = [specPath];
89
+ spinner.text = `Using spec file ${Colors.url(specPath)}}`;
90
+ }
91
+ if (!specPaths) {
92
+ spinner.fail(`No specs found`);
93
+ throw new Error('No specPaths defined or received from Downloads');
94
+ }
95
+ spinner.succeed(`Loaded specs`);
96
+ templatePath = path.resolve(Root.path(), templatePath);
97
+ Log.info(`Templates will be read from ${Colors.url(templatePath)}`);
98
+ outputPath = path.resolve(Root.path(), outputPath);
99
+ Log.info(`Rendered TypeScript files will be written to ${Colors.url(outputPath)}`);
100
+ spinner.start(`Checking for overrides`);
101
+ Overrides.setOutputPath(outputPath);
102
+ if (overridePath) {
103
+ overridePath = path.resolve(Root.path(), overridePath);
104
+ Overrides.setOverrides(JSON.parse(fs.readFileSync(overridePath).toString()));
105
+ spinner.succeed(`Overrides loaded from ${Colors.url(overridePath)}`);
106
+ }
107
+ else {
108
+ spinner.info(`No overrides`);
109
+ }
110
+ const { spec, models } = await Models.generate({
111
+ specPaths,
112
+ templatePath,
113
+ outputPath: path.join(outputPath, modelDirName)
114
+ });
115
+ const operations = await Operations.generate({
116
+ spec,
117
+ models,
118
+ templatePath,
119
+ outputPath: path.join(outputPath, operationsDirName)
120
+ });
121
+ if (map) {
122
+ fs.writeFileSync(path.join(outputPath, 'map.json'), JSON.stringify(operations, null, 2));
123
+ }
124
+ }
@@ -0,0 +1,19 @@
1
+ import { PathString } from '@battis/descriptive-types';
2
+ export type TSDeprecation = string | undefined;
3
+ export type TSExport = 'export' | '' | undefined;
4
+ export type TSName = string;
5
+ export type TSType = {
6
+ type: string;
7
+ tsReferences?: TSReference[];
8
+ optional?: '?';
9
+ description?: string;
10
+ };
11
+ export type TSReference = {
12
+ type: string;
13
+ } & ({
14
+ filePath?: PathString;
15
+ packagePath?: never;
16
+ } | {
17
+ packagePath: string;
18
+ filePath?: never;
19
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import { PathString } from '@battis/descriptive-types';
2
+ import * as Swagger from '@groton/swagger-spec-ts';
3
+ import { AnnotatedModel } from './Annotation.js';
4
+ import { TSDeprecation, TSExport, TSName, TSType } from './TSAnnotation.js';
5
+ export declare function toTSDeprecation(obj: object): TSDeprecation;
6
+ export declare function toTSExport(_: AnnotatedModel): TSExport;
7
+ export declare function toTSPropertyName(id: string): TSName;
8
+ export declare function toTSTypeName(id: string): TSName;
9
+ export declare function toTSNamespace(filePath: PathString): TSName;
10
+ export declare function toTSType(property: Swagger.v1p2.DataType): TSType;
@@ -0,0 +1,84 @@
1
+ import { Colors } from '@battis/qui-cli.colors';
2
+ import { Log } from '@battis/qui-cli.log';
3
+ import * as Swagger from '@groton/swagger-spec-ts';
4
+ import path from 'node:path';
5
+ import * as Overrides from './Overrides.js';
6
+ export function toTSDeprecation(obj) {
7
+ if ('deprecated' in obj && obj.deprecated) {
8
+ return `@deprecated ${'deprecation_description' in obj ? obj.deprecation_description : ''}`;
9
+ }
10
+ return undefined;
11
+ }
12
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
13
+ export function toTSExport(_) {
14
+ return 'export';
15
+ }
16
+ export function toTSPropertyName(id) {
17
+ if (/^[a-z_$][a-z_$0-9]*$/i.test(id)) {
18
+ return id;
19
+ }
20
+ return `"${id}"`;
21
+ }
22
+ export function toTSTypeName(id) {
23
+ return id.replace(/[^a-z0-9_]+/gi, '');
24
+ }
25
+ export function toTSNamespace(filePath) {
26
+ let tsNamespace = path
27
+ .basename(filePath, '.ts')
28
+ .replace(/[^a-z0-9_]+/gi, '_')
29
+ .split('_')
30
+ .map((token) => token.length ? token[0].toUpperCase() + token.slice(1) : '')
31
+ .join('');
32
+ if (tsNamespace === 'V1') {
33
+ tsNamespace = tsNamespace.toLowerCase();
34
+ }
35
+ return tsNamespace;
36
+ }
37
+ export function toTSType(property) {
38
+ if (Swagger.v1p2.isRefType(property)) {
39
+ return {
40
+ type: toTSTypeName(property.$ref),
41
+ tsReferences: [{ type: toTSTypeName(property.$ref) }]
42
+ };
43
+ }
44
+ let tsType = Overrides.tsType(property.type);
45
+ if (!tsType) {
46
+ tsType = { type: 'unknown' };
47
+ switch (property.type) {
48
+ case 'boolean':
49
+ case 'number':
50
+ tsType.description = `type: ${property.type}`;
51
+ tsType.type = `${property.type} | string`;
52
+ break;
53
+ case 'void':
54
+ case 'string':
55
+ tsType.type = property.type;
56
+ break;
57
+ case 'integer':
58
+ tsType.type = 'number | string';
59
+ tsType.description = `type: ${property.type}`;
60
+ break;
61
+ case 'array':
62
+ if ('items' in property) {
63
+ if ('$ref' in property.items && property.items.$ref === 'Array') {
64
+ tsType.type = 'string[]';
65
+ Log.debug(`Interpretting an array with ${Colors.value('items.$ref')}: ${Colors.quotedValue(`"Array"`)} as ${Colors.value(tsType.type)}`);
66
+ break;
67
+ }
68
+ else {
69
+ const itemType = toTSType(property.items);
70
+ tsType.type = `${itemType.type}[]`;
71
+ tsType.tsReferences = itemType.tsReferences;
72
+ }
73
+ }
74
+ break;
75
+ default:
76
+ Log.debug(`Interpretting ${Colors.value('type')}: ${Colors.quotedValue(`"${property.type}"`)} as ${Colors.value('RefType')}`);
77
+ return toTSType({ $ref: property.type });
78
+ }
79
+ }
80
+ if ('format' in property && property.format != null) {
81
+ tsType.description = `${tsType.description || ''}\n\nformat: '${property.format}'`;
82
+ }
83
+ return tsType;
84
+ }
@@ -0,0 +1,2 @@
1
+ import { PathString } from '@battis/descriptive-types';
2
+ export declare function importPath(from: PathString, to: PathString): string;
@@ -0,0 +1,9 @@
1
+ import path from 'node:path';
2
+ export function importPath(from, to) {
3
+ let relative = path.relative(path.dirname(from), to);
4
+ if (!/\//.test(relative)) {
5
+ relative = `./${relative}`;
6
+ }
7
+ relative = relative.replace(/\.ts$/, '.js');
8
+ return relative;
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import { Core } from '@battis/qui-cli.core';
2
+ import { register } from '@battis/qui-cli.plugin';
3
+ import * as Render from './Render.js';
4
+ await register(Render);
5
+ await Core.run();
@@ -0,0 +1,2 @@
1
+ import { PathString } from '@battis/descriptive-types';
2
+ export declare function writePrettier(filepath: PathString, content: string): Promise<void>;
@@ -0,0 +1,25 @@
1
+ import { Colors } from '@battis/qui-cli.colors';
2
+ import { Log } from '@battis/qui-cli.log';
3
+ import { Root } from '@battis/qui-cli.root';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import * as prettier from 'prettier';
7
+ export async function writePrettier(filepath, content) {
8
+ filepath = path.resolve(Root.path(), filepath);
9
+ const dir = path.dirname(filepath);
10
+ if (!fs.existsSync(dir)) {
11
+ fs.mkdirSync(dir, { recursive: true });
12
+ }
13
+ try {
14
+ fs.writeFileSync(filepath, content);
15
+ fs.writeFileSync(filepath, await prettier.format(content, {
16
+ filepath,
17
+ parser: 'typescript',
18
+ ...(await prettier.resolveConfig(filepath))
19
+ }));
20
+ }
21
+ catch (error) {
22
+ Log.error(`Error making ${Colors.url(filepath)} prettier: ${error.message}`, { prettierConfig: await prettier.resolveConfig(filepath) });
23
+ fs.writeFileSync(filepath, content);
24
+ }
25
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ import { build } from '@battis/qui-cli.structured';
2
+ await build({
3
+ fileName: import.meta.filename,
4
+ commandName: 'swagger-renderer'
5
+ });
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@groton/canvas-api.swagger-renderer",
3
+ "version": "0.1.0",
4
+ "description": "Render Canvas LMS Swagger 1.0 API documentation as TypeScript client",
5
+ "homepage": "https://github.com/groton-school/canvas-cli/tree/main/packages/api/swagger-renderer#readme",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/groton-school/canvas-cli.git",
9
+ "directory": "packages/api/swagger-renderer"
10
+ },
11
+ "author": {
12
+ "name": "Seth Battis",
13
+ "email": "sbattis@groton.org"
14
+ },
15
+ "type": "module",
16
+ "main": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "directories": {
19
+ "bin": "./bin"
20
+ },
21
+ "dependencies": {
22
+ "@battis/qui-cli.colors": "^2.1.0",
23
+ "@battis/qui-cli.core": "^3.1.0",
24
+ "@battis/qui-cli.log": "^2.2.2",
25
+ "@battis/qui-cli.plugin": "^2.4.2",
26
+ "@battis/qui-cli.root": "^2.0.5",
27
+ "@battis/qui-cli.structured": "^0.1.5",
28
+ "handlebars": "^4.7.8",
29
+ "node-fetch": "^3.3.2",
30
+ "ora": "^8.2.0",
31
+ "p-queue": "^8.1.0",
32
+ "prettier": "3.5.3",
33
+ "prettier-plugin-jsdoc": "^1.3.3",
34
+ "prettier-plugin-organize-imports": "^4.2.0"
35
+ },
36
+ "devDependencies": {
37
+ "@battis/descriptive-types": "^0.2.3",
38
+ "@battis/typescript-tricks": "^0.7.4",
39
+ "@tsconfig/node20": "^20.1.6",
40
+ "commit-and-tag-version": "^12.5.2",
41
+ "del-cli": "^6.0.0",
42
+ "npm-run-all": "^4.1.5",
43
+ "typescript": "^5.8.3",
44
+ "@groton/swagger-spec-ts": "0.1.0"
45
+ },
46
+ "scripts": {
47
+ "clean": "del ./dist",
48
+ "build": "run-s build:*",
49
+ "build:clean": "run-s clean",
50
+ "build:compile": "tsc",
51
+ "release": "commit-and-tag-version"
52
+ }
53
+ }