Compare commits
No commits in common. "main" and "v0.4.1" have entirely different histories.
5 changed files with 11 additions and 85 deletions
|
|
@ -48,55 +48,12 @@ for path, methods in paths.items():
|
||||||
# breaks strict consumers like Orval's Zod generator. Strip empty arrays —
|
# breaks strict consumers like Orval's Zod generator. Strip empty arrays —
|
||||||
# absence of the keyword has the same semantics.
|
# absence of the keyword has the same semantics.
|
||||||
schemas = spec.get("components", {}).get("schemas", {})
|
schemas = spec.get("components", {}).get("schemas", {})
|
||||||
stripped_required = 0
|
stripped = 0
|
||||||
for sch in schemas.values():
|
for sch in schemas.values():
|
||||||
if isinstance(sch, dict) and sch.get("required") == []:
|
if isinstance(sch, dict) and sch.get("required") == []:
|
||||||
del sch["required"]
|
del sch["required"]
|
||||||
stripped_required += 1
|
stripped += 1
|
||||||
|
|
||||||
# oatpp 1.3 emits `"type": "Any"` for `oatpp::Any` / `oatpp::Fields` fields.
|
|
||||||
# `Any` is not a valid OpenAPI 3.0 type — the spec uses an empty schema `{}`
|
|
||||||
# (or `nullable: true` with no type) to mean "any value". Replace `Any` with
|
|
||||||
# an empty schema; recurse so we catch nested cases (object property,
|
|
||||||
# array item, etc.).
|
|
||||||
fixed_any = 0
|
|
||||||
def fix_any(node):
|
|
||||||
global fixed_any
|
|
||||||
if isinstance(node, dict):
|
|
||||||
if node.get("type") == "Any":
|
|
||||||
del node["type"]
|
|
||||||
fixed_any += 1
|
|
||||||
for v in node.values():
|
|
||||||
fix_any(v)
|
|
||||||
elif isinstance(node, list):
|
|
||||||
for item in node:
|
|
||||||
fix_any(item)
|
|
||||||
fix_any(spec)
|
|
||||||
|
|
||||||
# oatpp 1.3 sometimes emits the same path parameter twice (same name + same
|
|
||||||
# `in: path`). OpenAPI 3.0 rejects duplicate parameters. Dedupe by (name, in)
|
|
||||||
# preserving first occurrence.
|
|
||||||
deduped_params = 0
|
|
||||||
for path_item in paths.values():
|
|
||||||
if not isinstance(path_item, dict):
|
|
||||||
continue
|
|
||||||
for op in path_item.values():
|
|
||||||
if not isinstance(op, dict) or "parameters" not in op:
|
|
||||||
continue
|
|
||||||
seen = set()
|
|
||||||
unique = []
|
|
||||||
for p in op["parameters"]:
|
|
||||||
key = (p.get("name"), p.get("in"))
|
|
||||||
if key in seen:
|
|
||||||
deduped_params += 1
|
|
||||||
continue
|
|
||||||
seen.add(key)
|
|
||||||
unique.append(p)
|
|
||||||
op["parameters"] = unique
|
|
||||||
|
|
||||||
SRC.write_text(json.dumps(spec, indent=2))
|
SRC.write_text(json.dumps(spec, indent=2))
|
||||||
report = [f"{len(paths)} paths"]
|
print(f" postprocessed {SRC} — {len(paths)} paths"
|
||||||
if stripped_required: report.append(f"stripped empty `required` from {stripped_required} schemas")
|
+ (f", stripped empty `required` from {stripped} schemas" if stripped else ""))
|
||||||
if fixed_any: report.append(f"replaced {fixed_any} invalid `type: Any` with any-schema")
|
|
||||||
if deduped_params: report.append(f"deduped {deduped_params} duplicate parameter(s)")
|
|
||||||
print(f" postprocessed {SRC} — " + ", ".join(report))
|
|
||||||
|
|
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@uschuster/webapp-scaffold",
|
"name": "@uschuster/webapp-scaffold",
|
||||||
"version": "0.5.0",
|
"version": "0.3.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@uschuster/webapp-scaffold",
|
"name": "@uschuster/webapp-scaffold",
|
||||||
"version": "0.5.0",
|
"version": "0.3.6",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"bin": {
|
"bin": {
|
||||||
"webapp-scaffold-fetch-openapi": "bin/fetch-openapi.sh",
|
"webapp-scaffold-fetch-openapi": "bin/fetch-openapi.sh",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@uschuster/webapp-scaffold",
|
"name": "@uschuster/webapp-scaffold",
|
||||||
"version": "0.5.0",
|
"version": "0.4.1",
|
||||||
"description": "Shared build scripts + Vite config factories for webapp-template-derived projects.",
|
"description": "Shared build scripts + Vite config factories for webapp-template-derived projects.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
||||||
|
|
@ -173,23 +173,4 @@ describe('createCoreFetch — request shape', () => {
|
||||||
await cf('/v1/x');
|
await cf('/v1/x');
|
||||||
expect(captured).toBe('https://api.example.com/v1/x');
|
expect(captured).toBe('https://api.example.com/v1/x');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('baseUrl getter is resolved at request time, not factory time', async () => {
|
|
||||||
const captured: string[] = [];
|
|
||||||
let dynamicBase = 'https://first.example.com';
|
|
||||||
const cf = createCoreFetch({
|
|
||||||
baseUrl: () => dynamicBase,
|
|
||||||
fetchImpl: ((url: string) => {
|
|
||||||
captured.push(url);
|
|
||||||
return Promise.resolve(jsonResponse({}));
|
|
||||||
}) as unknown as typeof fetch,
|
|
||||||
});
|
|
||||||
await cf('/v1/x');
|
|
||||||
dynamicBase = 'https://second.example.com/';
|
|
||||||
await cf('/v1/y');
|
|
||||||
expect(captured).toEqual([
|
|
||||||
'https://first.example.com/v1/x',
|
|
||||||
'https://second.example.com/v1/y',
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -28,16 +28,8 @@ export interface CoreFetchRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CoreFetchOptions {
|
export interface CoreFetchOptions {
|
||||||
/**
|
/** Prefix for every request; strip trailing slash. */
|
||||||
* Prefix for every request; trailing slash stripped at resolve time.
|
baseUrl?: string;
|
||||||
*
|
|
||||||
* Accepts a string OR a getter so consumers that derive the base from
|
|
||||||
* `import.meta.env.BASE_URL` (or any other late-bound source) can pass
|
|
||||||
* the getter form. The getter is invoked at request time, never at
|
|
||||||
* factory-construction time — which keeps orval's CJS bundling of the
|
|
||||||
* mutator file from blowing up on top-level `import.meta` references.
|
|
||||||
*/
|
|
||||||
baseUrl?: string | (() => string);
|
|
||||||
|
|
||||||
/** `'body'` returns the parsed body (default); `'wrapped'` returns { data, status, headers }. */
|
/** `'body'` returns the parsed body (default); `'wrapped'` returns { data, status, headers }. */
|
||||||
responseShape?: ResponseShape;
|
responseShape?: ResponseShape;
|
||||||
|
|
@ -105,11 +97,7 @@ export type CoreFetch = CoreFetchWrapped & CoreFetchBody;
|
||||||
const MUTATING = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
const MUTATING = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
||||||
|
|
||||||
export function createCoreFetch(opts: CoreFetchOptions = {}): CoreFetch {
|
export function createCoreFetch(opts: CoreFetchOptions = {}): CoreFetch {
|
||||||
// Lazy base resolution — see CoreFetchOptions.baseUrl JSDoc.
|
const base = (opts.baseUrl ?? '').replace(/\/$/, '');
|
||||||
const resolveBase = (): string => {
|
|
||||||
const raw = typeof opts.baseUrl === 'function' ? opts.baseUrl() : (opts.baseUrl ?? '');
|
|
||||||
return raw.replace(/\/$/, '');
|
|
||||||
};
|
|
||||||
// Resolve fetch at call time, not construction time, so tests that
|
// Resolve fetch at call time, not construction time, so tests that
|
||||||
// `vi.stubGlobal('fetch', ...)` after module import see the stub.
|
// `vi.stubGlobal('fetch', ...)` after module import see the stub.
|
||||||
const callFetch: typeof fetch = opts.fetchImpl
|
const callFetch: typeof fetch = opts.fetchImpl
|
||||||
|
|
@ -147,7 +135,7 @@ export function createCoreFetch(opts: CoreFetchOptions = {}): CoreFetch {
|
||||||
|
|
||||||
let r: Response;
|
let r: Response;
|
||||||
try {
|
try {
|
||||||
r = await callFetch(`${resolveBase()}${url}`, {
|
r = await callFetch(`${base}${url}`, {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
...init,
|
...init,
|
||||||
headers,
|
headers,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue