дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-13 21:25:47 +02:00
Порівняти коміти
1
Коміти
| Автор | SHA1 | Дата | |
|---|---|---|---|
|
|
adc89f3d13 |
No files matched your search
@@ -10,6 +10,9 @@ import { gutterIconMarkup, showContextMenu } from "@/ui";
|
||||
import { emptyGutterSpacer, gutterSpacer } from "@/editor/api";
|
||||
import { notifications, onEndpointsChanged, searchEndpoints, type EndpointEntry } from "@/workbench/contributions";
|
||||
import { appendGeneratedRequests } from "./generatedRequests";
|
||||
import { headerCompletion } from "./headers";
|
||||
import { sourceReader } from "./spring/editor";
|
||||
import { findHost } from "./spring/request";
|
||||
import { METHODS } from "./language";
|
||||
import styles from "./HttpClient.module.css";
|
||||
|
||||
@@ -17,13 +20,15 @@ export const OPEN_ENDPOINT = "http.openEndpoint";
|
||||
|
||||
export const endpointUrl = (path: string) => path.replace(/\{(?!\{)([\w.-]+)\}(?!\})/g, "{{$1}}");
|
||||
|
||||
export function renderEndpoint(e: EndpointEntry): string {
|
||||
return `### ${e.handler ?? `${e.method} ${e.url}`}\n${e.method === "*" ? "GET" : e.method} http://localhost:8080${endpointUrl(e.url)}\n`;
|
||||
export function renderEndpoint(e: EndpointEntry, host = "http://localhost:8080"): string {
|
||||
return `### ${e.handler ?? `${e.method} ${e.url}`}\n${e.method === "*" ? "GET" : e.method} ${host}${endpointUrl(e.url)}\n`;
|
||||
}
|
||||
|
||||
export async function openEndpoint(endpoint: EndpointEntry): Promise<void> {
|
||||
try {
|
||||
await appendGeneratedRequests([renderEndpoint(endpoint)]);
|
||||
// The server address of the endpoint's module (`server.port`, context path), as for Spring mappings.
|
||||
const host = await findHost(endpoint.path, useProject.getState().project?.root ?? null, sourceReader);
|
||||
await appendGeneratedRequests([renderEndpoint(endpoint, host)]);
|
||||
} catch (e) {
|
||||
notifications().notify({ level: "error", title: tx("Cannot generate the HTTP request"), body: String(e) });
|
||||
}
|
||||
@@ -94,7 +99,11 @@ class EndpointMarker extends GutterMarker {
|
||||
const endpointSpacer = gutterSpacer(styles.runMarker ?? "");
|
||||
|
||||
export function endpointEditorExtension(ctx: EditorDocContext): Extension {
|
||||
if (ctx.language === "http") return EditorState.languageData.of(() => [{ openideaCompletion: endpointCompletion }]);
|
||||
if (ctx.language === "http")
|
||||
return EditorState.languageData.of(() => [
|
||||
{ openideaCompletion: endpointCompletion },
|
||||
{ openideaCompletion: headerCompletion },
|
||||
]);
|
||||
if (ctx.virtual) return [];
|
||||
return [
|
||||
marks,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { headerTarget } from "./headers";
|
||||
|
||||
const at = (text: string) => headerTarget(text.replace("|", ""), text.indexOf("|"));
|
||||
|
||||
describe("header completion", () => {
|
||||
it("completes header names on the lines after the request line", () => {
|
||||
expect(at("POST http://localhost:8080/users\nCon|")).toEqual({ kind: "name", from: 33, prefix: "Con" });
|
||||
expect(at("### Create\n# @name create\nPOST {{host}}/users\nAccept: */*\n|")).toMatchObject({
|
||||
kind: "name",
|
||||
prefix: "",
|
||||
});
|
||||
expect(at("http://localhost/a\nX-Re|")).toMatchObject({ kind: "name", prefix: "X-Re" });
|
||||
});
|
||||
|
||||
it("completes the values of a header", () => {
|
||||
expect(at("POST http://x/a\nContent-Type: app|")).toEqual({
|
||||
kind: "value",
|
||||
name: "Content-Type",
|
||||
from: 30,
|
||||
prefix: "app",
|
||||
});
|
||||
expect(at("GET http://x/a\nAuthorization:|")).toMatchObject({ kind: "value", name: "Authorization", prefix: "" });
|
||||
});
|
||||
|
||||
it("stays out of request lines, bodies, comments and blocks without a request", () => {
|
||||
expect(at("POS|")).toBeNull();
|
||||
expect(at('POST http://x/a\nContent-Type: application/json\n\n{"a|')).toBeNull();
|
||||
expect(at("POST http://x/a\n# Acc|")).toBeNull();
|
||||
expect(at("@host = http://x\nAcc|")).toBeNull();
|
||||
expect(at("POST http://x/a\n\n###\nAcc|")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
/** Header name and value completion in `.http` request blocks (the lines after the request line). */
|
||||
import type { Completion, CompletionContext, CompletionResult } from "@codemirror/autocomplete";
|
||||
import { METHODS } from "./language";
|
||||
|
||||
const REQUEST_LINE = new RegExp(`^\\s*(?:(?:${METHODS.join("|")})\\s+\\S|https?://|\\{\\{)`);
|
||||
const COMMENT = /^\s*(?:#|\/\/)/;
|
||||
|
||||
const HEADERS = [
|
||||
"Accept",
|
||||
"Accept-Charset",
|
||||
"Accept-Encoding",
|
||||
"Accept-Language",
|
||||
"Access-Control-Request-Headers",
|
||||
"Access-Control-Request-Method",
|
||||
"Authorization",
|
||||
"Cache-Control",
|
||||
"Connection",
|
||||
"Content-Disposition",
|
||||
"Content-Encoding",
|
||||
"Content-Language",
|
||||
"Content-Length",
|
||||
"Content-Type",
|
||||
"Cookie",
|
||||
"Date",
|
||||
"Expect",
|
||||
"Forwarded",
|
||||
"From",
|
||||
"Host",
|
||||
"If-Match",
|
||||
"If-Modified-Since",
|
||||
"If-None-Match",
|
||||
"If-Range",
|
||||
"If-Unmodified-Since",
|
||||
"Max-Forwards",
|
||||
"Origin",
|
||||
"Pragma",
|
||||
"Proxy-Authorization",
|
||||
"Range",
|
||||
"Referer",
|
||||
"TE",
|
||||
"Upgrade",
|
||||
"User-Agent",
|
||||
"Via",
|
||||
"X-Correlation-ID",
|
||||
"X-CSRF-Token",
|
||||
"X-Forwarded-For",
|
||||
"X-Forwarded-Host",
|
||||
"X-Forwarded-Proto",
|
||||
"X-Request-ID",
|
||||
"X-Requested-With",
|
||||
];
|
||||
|
||||
const MEDIA_TYPES = [
|
||||
"application/json",
|
||||
"application/xml",
|
||||
"application/x-www-form-urlencoded",
|
||||
"multipart/form-data",
|
||||
"text/plain",
|
||||
"text/html",
|
||||
"text/xml",
|
||||
"text/csv",
|
||||
"application/octet-stream",
|
||||
"application/graphql",
|
||||
"application/x-ndjson",
|
||||
"application/problem+json",
|
||||
"*/*",
|
||||
];
|
||||
|
||||
const VALUES: Record<string, string[]> = {
|
||||
accept: MEDIA_TYPES,
|
||||
"content-type": MEDIA_TYPES,
|
||||
"accept-encoding": ["gzip, deflate, br", "gzip", "deflate", "br", "identity"],
|
||||
"content-encoding": ["gzip", "deflate", "br"],
|
||||
"accept-charset": ["utf-8", "iso-8859-1"],
|
||||
"accept-language": ["en-US", "en", "zh-CN", "zh"],
|
||||
authorization: ["Bearer ", "Basic "],
|
||||
"proxy-authorization": ["Bearer ", "Basic "],
|
||||
"cache-control": ["no-cache", "no-store", "max-age=0", "must-revalidate"],
|
||||
pragma: ["no-cache"],
|
||||
connection: ["keep-alive", "close", "Upgrade"],
|
||||
upgrade: ["websocket", "h2c"],
|
||||
expect: ["100-continue"],
|
||||
"x-requested-with": ["XMLHttpRequest"],
|
||||
"content-disposition": ["attachment", "inline", 'form-data; name=""'],
|
||||
"access-control-request-method": METHODS,
|
||||
};
|
||||
|
||||
export type HeaderTarget =
|
||||
| { kind: "name"; from: number; prefix: string }
|
||||
| { kind: "value"; name: string; from: number; prefix: string };
|
||||
|
||||
/** What is being typed at `pos`: a header name or a header's value, or null outside a request's headers. */
|
||||
export function headerTarget(text: string, pos: number): HeaderTarget | null {
|
||||
const before = text.slice(0, pos);
|
||||
const lineStart = before.lastIndexOf("\n") + 1;
|
||||
const line = before.slice(lineStart);
|
||||
if (COMMENT.test(line)) return null;
|
||||
const lines = (
|
||||
before
|
||||
.slice(0, lineStart)
|
||||
.split(/^\s*###.*$/m)
|
||||
.at(-1) ?? ""
|
||||
)
|
||||
.split("\n")
|
||||
.slice(0, -1);
|
||||
let request = false;
|
||||
for (const l of lines) {
|
||||
if (COMMENT.test(l)) continue;
|
||||
if (!request) request = REQUEST_LINE.test(l);
|
||||
// A blank line ends the headers; the body follows.
|
||||
else if (!l.trim()) return null;
|
||||
}
|
||||
if (!request) return null;
|
||||
const colon = line.indexOf(":");
|
||||
if (colon < 0) return /^[\w-]*$/.test(line) ? { kind: "name", from: lineStart, prefix: line } : null;
|
||||
const prefix = line.slice(colon + 1).replace(/^\s+/, "");
|
||||
return { kind: "value", name: line.slice(0, colon).trim(), from: pos - prefix.length, prefix };
|
||||
}
|
||||
|
||||
export function headerCompletion(context: CompletionContext): CompletionResult | null {
|
||||
const target = headerTarget(context.state.doc.toString(), context.pos);
|
||||
if (!target) return null;
|
||||
if (target.kind === "name") {
|
||||
if (!target.prefix && !context.explicit) return null;
|
||||
const options: Completion[] = HEADERS.map((name) => ({ label: name, type: "property", apply: `${name}: ` }));
|
||||
return { from: target.from, options, validFor: /^[\w-]*$/ };
|
||||
}
|
||||
const values = VALUES[target.name.toLowerCase()];
|
||||
if (!values) return null;
|
||||
return { from: target.from, options: values.map((v) => ({ label: v, type: "constant" })) };
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export const SPRING_LANGUAGES = new Set(["java", "kotlin"]);
|
||||
export const GENERATE_REQUEST = "http.generateSpringRequest";
|
||||
export const GENERATE_CONTROLLER_REQUESTS = "http.generateSpringControllerRequests";
|
||||
|
||||
const reader: SourceReader = {
|
||||
export const sourceReader: SourceReader = {
|
||||
read: async (path) => {
|
||||
try {
|
||||
return await call(commands.fsReadText(path));
|
||||
@@ -29,6 +29,7 @@ const reader: SourceReader = {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
filesNamed: (name) => call(commands.findFilesNamed(name)),
|
||||
};
|
||||
|
||||
/** One request per endpoint (`paths[i]` picks another path of the mapping). */
|
||||
@@ -43,12 +44,12 @@ export async function generateRequests(sourcePath: string, text: string, targets
|
||||
try {
|
||||
const root = useProject.getState().project?.root ?? null;
|
||||
const [host, lookup] = await Promise.all([
|
||||
findHost(sourcePath, root, reader),
|
||||
findHost(sourcePath, root, sourceReader),
|
||||
bodyLookup(
|
||||
sourcePath,
|
||||
parseSource(text),
|
||||
targets.map((t) => t.endpoint),
|
||||
reader,
|
||||
sourceReader,
|
||||
),
|
||||
]);
|
||||
const blocks = targets.map(({ endpoint, path }) =>
|
||||
|
||||
@@ -115,9 +115,18 @@ export function skeleton(type: string, lookup: (name: string) => ClassDecl | nul
|
||||
if (MAP.test(t) || t === "Object" || t === "Any" || !t) return {};
|
||||
const cls = lookup(t.replace(/<.*$/, ""));
|
||||
if (!cls || depth > 1) return {};
|
||||
// Superclass properties first, as Jackson writes them.
|
||||
const chain = [cls];
|
||||
for (let c = cls; c.superclass && chain.length <= MAX_SUPERCLASSES;) {
|
||||
const parent = lookup(bareType(c.superclass).replace(/<.*$/, ""));
|
||||
if (!parent || chain.includes(parent)) break;
|
||||
chain.unshift((c = parent));
|
||||
}
|
||||
const obj: { [k: string]: Json } = {};
|
||||
for (const p of cls.ctorParams) obj[p.name] = skeleton(p.type, lookup, depth + 1);
|
||||
for (const f of cls.fields) if (!f.isStatic && !(f.name in obj)) obj[f.name] = skeleton(f.type, lookup, depth + 1);
|
||||
for (const c of chain) {
|
||||
for (const p of c.ctorParams) if (!(p.name in obj)) obj[p.name] = skeleton(p.type, lookup, depth + 1);
|
||||
for (const f of c.fields) if (!f.isStatic && !(f.name in obj)) obj[f.name] = skeleton(f.type, lookup, depth + 1);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -175,8 +184,13 @@ export function appendRequests(existing: string, blocks: string[]): { insert: st
|
||||
export interface SourceReader {
|
||||
/** File text, null when missing. */
|
||||
read(path: string): Promise<string | null>;
|
||||
/** The project's files called `name` (from the index). */
|
||||
filesNamed?(name: string): Promise<string[]>;
|
||||
}
|
||||
|
||||
/** How many superclasses of a body type are followed for their fields. */
|
||||
const MAX_SUPERCLASSES = 5;
|
||||
|
||||
const dirname = (p: string) => p.replace(/[\\/][^\\/]*$/, "");
|
||||
|
||||
/** The Spring Boot configuration files for `sourcePath` (its module's `src/main/resources`), most specific first. */
|
||||
@@ -201,8 +215,10 @@ export async function findHost(sourcePath: string, projectRoot: string | null, f
|
||||
}
|
||||
|
||||
/**
|
||||
* Body types of `endpoints`: classes of the controller's file, then
|
||||
* `<Type>.java|.kt` beside it or where its import points (same source root).
|
||||
* Body types of `endpoints`: classes of the controller's file, then `<Type>.java|.kt` beside
|
||||
* it or where its import points (same source root), else the project file of that name in the
|
||||
* imported (or same / wildcard-imported) package, e.g. a DTO in another Maven module. Their
|
||||
* superclasses are loaded too.
|
||||
*/
|
||||
export async function bodyLookup(
|
||||
sourcePath: string,
|
||||
@@ -211,42 +227,78 @@ export async function bodyLookup(
|
||||
fs: SourceReader,
|
||||
): Promise<(name: string) => ClassDecl | null> {
|
||||
const known = new Map<string, ClassDecl>();
|
||||
const add = (f: SourceFile) => {
|
||||
for (const c of allClasses(f)) if (!known.has(c.name)) known.set(c.name, c);
|
||||
/** Where each class was read: names in its declarations resolve from that file. */
|
||||
const origins = new Map<string, Origin>();
|
||||
const add = (o: Origin) => {
|
||||
for (const c of allClasses(o.file))
|
||||
if (!known.has(c.name)) {
|
||||
known.set(c.name, c);
|
||||
origins.set(c.name, o);
|
||||
}
|
||||
};
|
||||
add(file);
|
||||
const origin = { path: sourcePath.replace(/\\/g, "/"), file };
|
||||
add(origin);
|
||||
const sourceRoot = (path: string, f: SourceFile) => {
|
||||
let root = dirname(path);
|
||||
for (let i = f.packageName ? f.packageName.split(".").length : 0; i > 0; i--) root = dirname(root);
|
||||
return root;
|
||||
};
|
||||
const ext = /\.kt$/.test(sourcePath) ? [".kt", ".java"] : [".java", ".kt"];
|
||||
/** Loads `name` as seen from `from` (its imports, then its directory). */
|
||||
const load = async (name: string, from: { path: string; file: SourceFile }) => {
|
||||
if (known.has(name) || !/^[A-Z]\w*$/.test(name)) return null;
|
||||
const readAt = async (path: string) => {
|
||||
const text = await fs.read(path).catch(() => null);
|
||||
if (text === null) return null;
|
||||
const loaded = { path, file: parseSource(text) };
|
||||
add(loaded);
|
||||
return loaded;
|
||||
};
|
||||
/** Loads `name` as seen from `from` (its imports, then its package). */
|
||||
const load = async (name: string, from: Origin) => {
|
||||
if (known.has(name)) return origins.get(name) ?? null;
|
||||
if (!/^[A-Z]\w*$/.test(name)) return null;
|
||||
const imported = from.file.imports.find((i) => i.endsWith(`.${name}`));
|
||||
const dir = dirname(from.path);
|
||||
const pkgDir = imported && `${sourceRoot(from.path, from.file)}/${imported.split(".").slice(0, -1).join("/")}`;
|
||||
for (const d of pkgDir ? [pkgDir, dir] : [dir])
|
||||
const packages = imported
|
||||
? [imported.split(".").slice(0, -1).join(".")]
|
||||
: [from.file.packageName, ...from.file.imports.filter((i) => i.endsWith(".*")).map((i) => i.slice(0, -2))];
|
||||
const root = sourceRoot(from.path, from.file);
|
||||
const dirs = new Set([...packages.map((p) => (p ? `${root}/${p.replace(/\./g, "/")}` : root)), dirname(from.path)]);
|
||||
for (const d of dirs)
|
||||
for (const x of ext) {
|
||||
const path = `${d}/${name}${x}`;
|
||||
const text = await fs.read(path).catch(() => null);
|
||||
if (text === null) continue;
|
||||
const loaded = parseSource(text);
|
||||
add(loaded);
|
||||
return { path, file: loaded };
|
||||
const loaded = await readAt(`${d}/${name}${x}`);
|
||||
if (loaded) return loaded;
|
||||
}
|
||||
// Another source root or module: the project's files of that name, in one of those packages.
|
||||
if (!fs.filesNamed) return null;
|
||||
const suffixes = packages.map((p) => `/${p ? `${p.replace(/\./g, "/")}/` : ""}${name}`);
|
||||
for (const x of ext) {
|
||||
const paths = (await fs.filesNamed(`${name}${x}`).catch(() => [])).map((p) => p.replace(/\\/g, "/"));
|
||||
const path = paths.find((p) => suffixes.some((s) => p.endsWith(`${s}${x}`)));
|
||||
const loaded = path ? await readAt(path) : null;
|
||||
if (loaded) return loaded;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const typeName = (t: string) => bareType(t).replace(/<.*$/, "");
|
||||
const origin = { path: sourcePath.replace(/\\/g, "/"), file };
|
||||
/** `name`, its superclasses, and the property types of each, one level down. */
|
||||
const loadWithProperties = async (name: string, from: Origin, depth: number) => {
|
||||
const seen = new Set<string>();
|
||||
let [current, at]: [string | undefined, Origin | null] = [name, from];
|
||||
while (current && at && !seen.has(current) && seen.size < MAX_SUPERCLASSES) {
|
||||
seen.add(current);
|
||||
const loaded = await load(current, at);
|
||||
const c = known.get(current);
|
||||
if (!c || !loaded) break;
|
||||
if (depth > 0)
|
||||
for (const p of [...c.ctorParams, ...c.fields.filter((f) => !f.isStatic)])
|
||||
await loadWithProperties(typeName(p.type), loaded, depth - 1);
|
||||
[current, at] = [c.superclass && typeName(c.superclass), loaded];
|
||||
}
|
||||
};
|
||||
const names = new Set(endpoints.flatMap((e) => (e.bodyType ? [typeName(e.bodyType)] : [])));
|
||||
for (const n of names) {
|
||||
const from = (await load(n, origin)) ?? origin;
|
||||
// Property types one level down, as seen from the body type's file.
|
||||
const c = known.get(n);
|
||||
for (const p of c ? [...c.ctorParams, ...c.fields.filter((f) => !f.isStatic)] : [])
|
||||
await load(typeName(p.type), from);
|
||||
}
|
||||
for (const n of names) await loadWithProperties(n, origin, 1);
|
||||
return (name) => known.get(name) ?? null;
|
||||
}
|
||||
|
||||
interface Origin {
|
||||
path: string;
|
||||
file: SourceFile;
|
||||
}
|
||||
@@ -65,6 +65,8 @@ export interface ClassDecl {
|
||||
/** Java record components / Kotlin primary constructor parameters (`val`/`var` ones are properties). */
|
||||
ctorParams: Param[];
|
||||
classes: ClassDecl[];
|
||||
/** The class it extends (`extends Base<T>` / Kotlin `: Base()`), as written. */
|
||||
superclass?: string;
|
||||
}
|
||||
|
||||
export interface SourceFile {
|
||||
@@ -220,6 +222,26 @@ class Reader {
|
||||
return !!t && t.text === text && (t.kind === "p" || t.kind === "id");
|
||||
}
|
||||
|
||||
/** A type name at `i` (`a.b.Base<T>`): its text and the index after it. */
|
||||
typeName(i: number, to: number): { name: string; end: number } | null {
|
||||
let j = i;
|
||||
while (j < to && this.toks[j]?.kind === "id" && (j === i || this.is(j - 1, "."))) {
|
||||
j++;
|
||||
if (!this.is(j, ".") || this.toks[j + 1]?.kind !== "id") break;
|
||||
j++;
|
||||
}
|
||||
if (j === i) return null;
|
||||
if (this.is(j, "<")) {
|
||||
let depth = 0;
|
||||
for (; j < to; j++) {
|
||||
if (this.is(j, "<")) depth++;
|
||||
else if (this.is(j, ">") && --depth === 0) break;
|
||||
}
|
||||
j++;
|
||||
}
|
||||
return { name: joined(this.toks.slice(i, j)), end: j };
|
||||
}
|
||||
|
||||
/** Kotlin: a token on a new line that starts a member ends the previous one. */
|
||||
startsMember(i: number): boolean {
|
||||
const t = this.toks[i];
|
||||
@@ -415,6 +437,12 @@ class Reader {
|
||||
}
|
||||
for (; j < to; j++) {
|
||||
if (this.is(j, "{") || this.is(j, ";") || this.startsMember(j)) break;
|
||||
// Java `extends Base`; Kotlin `: Base(…)` (a supertype called with arguments is the class).
|
||||
const javaSuper = this.toks[j]!.kind === "id" && this.toks[j]!.text === "extends";
|
||||
if (!cls.superclass && (javaSuper || this.is(j, ":") || this.is(j, ","))) {
|
||||
const type = this.typeName(j + 1, to);
|
||||
if (type && (javaSuper || this.is(type.end, "("))) cls.superclass = type.name;
|
||||
}
|
||||
if (OPEN[this.toks[j]!.text] && this.toks[j]!.kind === "p") j = this.match[j]!;
|
||||
}
|
||||
if (this.is(j, "{")) {
|
||||
|
||||
@@ -271,6 +271,41 @@ describe("body types from other files", () => {
|
||||
expect(skeleton("List<CreateUser>", lookup)).toEqual([]);
|
||||
expect(skeleton("Unknown", lookup)).toEqual({});
|
||||
});
|
||||
|
||||
it("finds a DTO in another module through the index, with its superclass fields", async () => {
|
||||
const files: Record<string, string> = {
|
||||
"/ws/common/src/main/java/com/example/demo/dto/CreateUser.java":
|
||||
"package com.example.demo.dto;\nimport com.example.base.BaseEntity;\n@Data\npublic class CreateUser extends BaseEntity<Long> implements Serializable {\n private String name;\n private Address address;\n}\n",
|
||||
"/ws/common/src/main/java/com/example/demo/dto/Address.java":
|
||||
"package com.example.demo.dto;\npublic class Address { private String city; }\n",
|
||||
"/ws/base/src/main/java/com/example/base/BaseEntity.java":
|
||||
"package com.example.base;\npublic abstract class BaseEntity<ID> { private ID id; private LocalDateTime createdAt; }\n",
|
||||
// Same name, other package: not the imported class.
|
||||
"/ws/legacy/src/main/java/com/legacy/CreateUser.java":
|
||||
"package com.legacy;\npublic class CreateUser { int old; }\n",
|
||||
};
|
||||
const fs: SourceReader = {
|
||||
read: async (p) => files[p] ?? null,
|
||||
filesNamed: async (name) => Object.keys(files).filter((p) => p.endsWith(`/${name}`)),
|
||||
};
|
||||
const eps = springControllers(JAVA)[0]!.endpoints;
|
||||
const lookup = await bodyLookup(
|
||||
"/ws/web/src/main/java/com/example/demo/web/UserController.java",
|
||||
parseSource(JAVA),
|
||||
eps,
|
||||
fs,
|
||||
);
|
||||
expect(skeleton("CreateUser", lookup)).toEqual({ id: {}, createdAt: "", name: "", address: { city: "" } });
|
||||
});
|
||||
|
||||
it("reads Java and Kotlin superclasses", () => {
|
||||
const java = parseSource("class A extends com.x.Base<T> implements I { }\ninterface J extends K {}");
|
||||
expect(java.classes.map((c) => c.superclass)).toEqual(["com.x.Base<T>", "K"]);
|
||||
const kotlin = parseSource(
|
||||
"class A(val a: Int) : Base(a), Serializable\nclass B : Serializable, Other()\nclass C : I",
|
||||
);
|
||||
expect(kotlin.classes.map((c) => c.superclass)).toEqual(["Base", "Other", undefined]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("appending to the request file", () => {
|
||||
|
||||
Reference in new issue
Заблокувати користувача