cargo-json-docs 0.5.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.
package/src/json.ts ADDED
@@ -0,0 +1,274 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { spawnSync } from 'child_process';
4
+
5
+ export interface CrateMetaData {
6
+ name: string;
7
+ version: string;
8
+ documentation: string;
9
+ }
10
+
11
+ /**
12
+ * Reads the Cargo.toml file to get the crate metadata.
13
+ * @param cargoTomlPath The path to the Cargo.toml file.
14
+ * @returns The crate metadata, or null if not found.
15
+ */
16
+ export function getCargoMeta(cargoTomlPath: string): CrateMetaData | null {
17
+ if (!fs.existsSync(cargoTomlPath)) {
18
+ return null;
19
+ }
20
+
21
+ const cargoTomlContent = fs.readFileSync(cargoTomlPath, 'utf-8');
22
+
23
+ const packageSection = cargoTomlContent.match(/\[package\]([\s\S]*?)(\n\[|$)/);
24
+ if (!packageSection) return null;
25
+ const section = packageSection[1]!;
26
+
27
+
28
+ const nameMatch = section.match(/^\s*name\s*=\s*["']([^"']+)["']/m);
29
+ if (!nameMatch) {
30
+ return null;
31
+ }
32
+ const name = nameMatch[1]!;
33
+
34
+ const versionMatch = section.match(/^\s*version\s*=\s*["']([^"']+)["']/m);
35
+ const version = versionMatch ? versionMatch[1]! : '0.0.0';
36
+
37
+ const documentationMatch = section.match(/^\s*documentation\s*=\s*["']([^"']+)["']/m);
38
+ const documentation = documentationMatch ? documentationMatch[1]! : `https://docs.rs/crate/${name}/latest`;
39
+
40
+ let meta: CrateMetaData = { name, version, documentation };
41
+ return meta;
42
+ }
43
+
44
+ /**
45
+ * Recursively walks a directory to find the latest modification time among all files and subdirectories.
46
+ * @param dirPath The directory path to walk.
47
+ * @returns The latest modification date found.
48
+ */
49
+ function walkDirForLatestModTime(dirPath: string): Date {
50
+ let latestModTime = new Date(0);
51
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
52
+
53
+ for (const entry of entries) {
54
+ const fullPath = path.join(dirPath, entry.name);
55
+ const stats = fs.statSync(fullPath);
56
+ if (stats.isSymbolicLink()) continue;
57
+
58
+ if (stats.mtime > latestModTime) {
59
+ latestModTime = stats.mtime;
60
+ }
61
+
62
+ if (entry.isDirectory()) {
63
+ const dirModTime = walkDirForLatestModTime(fullPath);
64
+ if (dirModTime > latestModTime) {
65
+ latestModTime = dirModTime;
66
+ }
67
+ }
68
+ }
69
+
70
+ return latestModTime;
71
+ }
72
+
73
+ /**
74
+ * Gets the path to the cargo binary.
75
+ * Checks CARGO_HOME environment variable, defaults to ~/.cargo/bin/cargo.
76
+ * @returns The path to the cargo binary, or null if not found.
77
+ */
78
+ let cachedCargoPath: string | null = null;
79
+ export function getCargoBinPath(): string | null {
80
+ if (cachedCargoPath !== null) {
81
+ return cachedCargoPath;
82
+ }
83
+
84
+ // First run the command 'where cargo' (Windows) or 'which cargo' (Unix)
85
+ const isWindows = process.platform === 'win32';
86
+ const command = isWindows ? 'where' : 'which';
87
+ const result = spawnSync(command, ['cargo'], { encoding: 'utf-8' });
88
+ if (result.status === 0) {
89
+ const cargoPath = result.stdout.split('\n')[0]!.trim();
90
+ return cargoPath;
91
+ }
92
+
93
+ // Fallback to CARGO_HOME or default path
94
+ const cargoHome = process.env.CARGO_HOME || path.join(process.env.HOME || process.env.USERPROFILE || '', '.cargo');
95
+ const cargoBin = path.join(cargoHome, 'bin', isWindows ? 'cargo.exe' : 'cargo');
96
+ if (fs.existsSync(cargoBin)) {
97
+ cachedCargoPath = cargoBin;
98
+ return cargoBin;
99
+ }
100
+
101
+ return null;
102
+ }
103
+
104
+ // Cache exe path
105
+
106
+ /**
107
+ * Represents a source of documentation for a Rust crate.
108
+ *
109
+ * Note: Crate - not workspace. I don't currently support workspaces.
110
+ * PRs welcome to add this feature.
111
+ */
112
+ export class DocumentationSource {
113
+ /**
114
+ * The path to the crate source.
115
+ */
116
+ public path: string;
117
+
118
+ /**
119
+ * The name of the crate.
120
+ */
121
+ public name: string;
122
+
123
+ /**
124
+ * The root URL for the crate's documentation.
125
+ */
126
+ public docs_root: string;
127
+
128
+ /**
129
+ * Creates a new DocumentationSource instance.
130
+ * @param crate_path The path to the crate source directory (where Cargo.toml is located).
131
+ */
132
+ constructor(crate_path: string) {
133
+ this.path = crate_path;
134
+
135
+ const cargoTomlPath = path.join(this.path, 'Cargo.toml');
136
+ const metadata = getCargoMeta(cargoTomlPath);
137
+ if (metadata === null) {
138
+ throw new Error(`Could not determine crate name from ${cargoTomlPath}`);
139
+ }
140
+
141
+ this.name = metadata.name;
142
+ this.docs_root = metadata.documentation;
143
+ }
144
+
145
+ /**
146
+ * Gets the date and time the crate was last modified.
147
+ * Checks Cargo.toml and the src/ directory.
148
+ * @returns The last modification date.
149
+ */
150
+ lastModified(): Date {
151
+ let cargoTomlStat = fs.statSync(path.join(this.path, 'Cargo.toml'));
152
+ let srcDirStat = fs.statSync(path.join(this.path, 'src'));
153
+
154
+ let lastModified = cargoTomlStat.mtime;
155
+ let srcLastModified = walkDirForLatestModTime(path.join(this.path, 'src'));
156
+
157
+ if (srcLastModified > lastModified) {
158
+ lastModified = srcLastModified;
159
+ }
160
+
161
+ return lastModified;
162
+ }
163
+
164
+ /**
165
+ * Gets the date and time the documentation JSON was last modified.
166
+ * Checks for a *.json file in the doc directory.
167
+ * @returns The last modification date, or null if no JSON file found.
168
+ */
169
+ jsonModified(): Date | null {
170
+ let file = this.getJsonPath();
171
+ if (file === null) return null;
172
+
173
+ let stat = fs.statSync(file);
174
+ if (!stat) return null;
175
+ return stat.mtime;
176
+ }
177
+
178
+ /**
179
+ * Determines if the documentation JSON should be regenerated.
180
+ * Compares the last modified time of the crate source and the JSON file.
181
+ * @returns True if the JSON should be regenerated, false otherwise.
182
+ */
183
+ shouldGenerate(): boolean {
184
+ let jsonMod = this.jsonModified();
185
+ if (jsonMod === null) {
186
+ return true;
187
+ }
188
+
189
+ let lastMod = this.lastModified();
190
+ return lastMod > jsonMod;
191
+ }
192
+
193
+ /**
194
+ * Gets the path to the documentation directory.
195
+ * @returns The path to the documentation directory.
196
+ */
197
+ getDocPath(): string {
198
+ // Check for CARGO_TARGET_DIR environment variable
199
+ let cargoTargetDir = process.env.CARGO_TARGET_DIR;
200
+ let docDir: string = path.join(this.path, 'target', 'doc');
201
+ if (cargoTargetDir) {
202
+ docDir = path.join(cargoTargetDir, 'doc');
203
+ }
204
+
205
+ return docDir;
206
+ }
207
+
208
+ /**
209
+ * Gets the path to the documentation JSON file.
210
+ * @returns The path to the JSON file, or null if not found.
211
+ */
212
+ getJsonPath(): string | null {
213
+ let docDir = this.getDocPath();
214
+ if (!fs.existsSync(docDir)) {
215
+ return null;
216
+ }
217
+
218
+ let files = fs.readdirSync(docDir);
219
+ let jsonFiles = files.filter((f: string) => f == `${this.name}.json`);
220
+ if (jsonFiles.length === 0) {
221
+ return null;
222
+ }
223
+
224
+ return path.join(docDir, jsonFiles[0]!);
225
+ }
226
+
227
+ /**
228
+ * Calls the cargo binary with the specified arguments.
229
+ * @param args The arguments to pass to cargo.
230
+ * @returns The exit code of the cargo process.
231
+ */
232
+ callCargo(args: string[]): number {
233
+ let bin = getCargoBinPath();
234
+ if (bin === null) {
235
+ throw new Error('Cargo binary not found.');
236
+ }
237
+
238
+ let result = spawnSync(bin, args, {
239
+ cwd: this.path,
240
+ stdio: 'inherit',
241
+ });
242
+
243
+ return result.status || 0;
244
+ }
245
+
246
+ /**
247
+ * Generates the documentation JSON using `cargo +nighly rustdoc --lub -- -Z unstable-options --output-format json`.
248
+ */
249
+ generateJson(): void {
250
+ let result = this.callCargo(['+nightly', 'rustdoc', '--lib', '--', '-Z', 'unstable-options', '--output-format', 'json']);
251
+ console.log(`Cargo rustdoc exited with code ${result}`);
252
+ if (result !== 0) {
253
+ throw new Error('Failed to generate documentation JSON.');
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Gets the documentation JSON, generating it if necessary.
259
+ * @returns The parsed JSON object.
260
+ */
261
+ getJson(): any {
262
+ if (this.shouldGenerate()) {
263
+ this.generateJson();
264
+ }
265
+
266
+ let jsonPath = this.getJsonPath();
267
+ if (jsonPath === null) {
268
+ throw new Error('Documentation JSON file not found after generation.');
269
+ }
270
+
271
+ let jsonData = fs.readFileSync(jsonPath, 'utf-8');
272
+ return JSON.parse(jsonData);
273
+ }
274
+ }
package/src/lib.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { DocumentationSource } from "./json";
2
+ import { CrateDocs } from "./item";
3
+ import { ResolutionError, CargoFormatError } from "./errors";
4
+
5
+ export { DocumentationSource, CrateDocs, ResolutionError, CargoFormatError };
6
+
7
+ /**
8
+ * Loads a crate's documentation from source.
9
+ * @param name The name of the crate.
10
+ * @param docs_root The root URL for the crate's documentation.
11
+ * @param path The path to the crate's source directory (e.g., the directory containing Cargo.toml).
12
+ * @returns The loaded CrateDocs object.
13
+ */
14
+ export function loadCrate(path: string): CrateDocs {
15
+ const docSource = new DocumentationSource(path);
16
+ const jsonSource = docSource.getJson();
17
+ return new CrateDocs(docSource.name, docSource.docs_root, jsonSource);
18
+ }
19
+
20
+ /**
21
+ * Loads a crate's documentation from a JSON string.
22
+ * @param name The name of the crate.
23
+ * @param docs_root The root URL for the crate's documentation.
24
+ * @param json The JSON string containing the documentation data.
25
+ * @returns The loaded CrateDocs object.
26
+ */
27
+ export function loadCrateFromJson(name: string, docs_root: string, json: string): CrateDocs {
28
+ const source = JSON.parse(json);
29
+ return new CrateDocs(name, docs_root, source);
30
+ }
package/src/types.d.ts ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Rust documentation JSON types.
3
+ */
4
+ export type ItemKind = 'crate' | 'module' | 'struct' | 'enum' | 'function' | 'functiontype' | 'trait' | 'macro' | 'variant' | 'struct_field' | 'impl' | 'use' | 'assoc_type' | 'assoc_const' | 'constant' | 'type_alias';
5
+
6
+ /**
7
+ * A documented item in Rust documentation JSON.
8
+ */
9
+ export interface DocumentedItem {
10
+ /**
11
+ * The name of the item.
12
+ */
13
+ name: string;
14
+ /**
15
+ * The full path to the item (e.g., module::SubModule::ItemName).
16
+ */
17
+ path: string;
18
+ /**
19
+ * Markdown documentation for the item, or null if none.
20
+ */
21
+ docs: string | null;
22
+ /**
23
+ * The kind of item (e.g., struct, enum, function). See ItemKind.
24
+ */
25
+ kind: ItemKind;
26
+ /**
27
+ * The URL to the item's documentation page.
28
+ */
29
+ url: string;
30
+ }
31
+
32
+ export interface ModuleProps {
33
+ is_crate: boolean;
34
+ items: number[];
35
+ }
36
+
37
+ export interface UseProps {
38
+ name: string;
39
+ id: number;
40
+ is_glob: boolean;
41
+ }
42
+
43
+ export interface StructProps {
44
+ impls: number[];
45
+ kind: any;
46
+ }
47
+
48
+ export interface EnumProps {
49
+ variants: number[];
50
+ impls: number[];
51
+ }
52
+
53
+ export interface FunctionProps {
54
+ has_body: boolean;
55
+ }
56
+
57
+ export interface TraitProps {
58
+ items: number[];
59
+ }
60
+
61
+ export interface IndexEntry {
62
+ id: number;
63
+ is_crate: boolean;
64
+ crate_id: number;
65
+ name: string;
66
+ kind: ItemKind;
67
+ docs: string | null;
68
+ silent: boolean;
69
+ inner: any;
70
+ }
71
+
72
+ export interface CrateEntry {
73
+ id: string;
74
+ name: string;
75
+ html_root_url: string;
76
+ could_be_root_module: boolean;
77
+ }
78
+
79
+ export interface PathEntry {
80
+ crate_id: number;
81
+ path: string[];
82
+ kind: ItemKind;
83
+ silent: boolean;
84
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=item.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"item.test.d.ts","sourceRoot":"","sources":["item.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,107 @@
1
+ import { test, expect, afterAll } from 'vitest';
2
+ import fs from 'fs';
3
+ import fetch from 'node-fetch';
4
+ import { loadCrate } from '../src/lib';
5
+ import { DocumentationSource } from '../src/json';
6
+ const crateDocs = loadCrate('test/test_crate');
7
+ let testSet = {
8
+ 'functions': [
9
+ { path: 'add', kind: 'function', url: 'https://docs.rs/crate/test_crate/latest/test_crate/fn.add.html' },
10
+ { path: 'test_crate::add', kind: 'function', url: 'https://docs.rs/crate/test_crate/latest/test_crate/fn.add.html' }
11
+ ],
12
+ 'external': [
13
+ { path: 'Serialize', kind: 'trait', url: 'https://docs.rs/crate/test_crate/latest/test_crate/trait.Serialize.html' }
14
+ ],
15
+ 'structures': [
16
+ { path: 'TestStructPlain', kind: 'struct', url: 'https://docs.rs/crate/test_crate/latest/test_crate/struct.TestStructPlain.html' },
17
+ { path: 'TestStructPlain::new', kind: 'function', url: 'https://docs.rs/crate/test_crate/latest/test_crate/struct.TestStructPlain.html#method.new' },
18
+ { path: 'TestStructPlain::field1', kind: 'struct_field', url: 'https://docs.rs/crate/test_crate/latest/test_crate/struct.TestStructPlain.html#structfield.field1' },
19
+ { path: 'TestStructTuple::0', kind: 'struct_field', url: 'https://docs.rs/crate/test_crate/latest/test_crate/struct.TestStructTuple.html#structfield.0' }
20
+ ],
21
+ 'enums': [
22
+ { path: 'TestEnum', kind: 'enum', url: 'https://docs.rs/crate/test_crate/latest/test_crate/enum.TestEnum.html' },
23
+ { path: 'TestEnum::VariantThree', kind: 'variant', url: 'https://docs.rs/crate/test_crate/latest/test_crate/enum.TestEnum.html#variant.VariantThree' },
24
+ { path: 'TestEnum::describe', kind: 'function', url: 'https://docs.rs/crate/test_crate/latest/test_crate/enum.TestEnum.html#method.describe' }
25
+ ],
26
+ 'modules': [
27
+ { path: 'module_b', kind: 'module', url: 'https://docs.rs/crate/test_crate/latest/test_crate/module_b/index.html' },
28
+ { path: 'module_b::inner_b', kind: 'module', url: 'https://docs.rs/crate/test_crate/latest/test_crate/module_b/inner_b/index.html' }
29
+ ],
30
+ 'traits': [
31
+ { path: 'module_b::inner_b::InnerBTrait', kind: 'trait', url: 'https://docs.rs/crate/test_crate/latest/test_crate/module_b/inner_b/trait.InnerBTrait.html' },
32
+ { path: 'module_b::inner_b::InnerBTrait::AssociatedType', kind: 'assoc_type', url: 'https://docs.rs/crate/test_crate/latest/test_crate/module_b/inner_b/trait.InnerBTrait.html#associatedtype.AssociatedType' },
33
+ { path: 'module_b::inner_b::InnerBTrait::ASSOCIATED_CONST', kind: 'assoc_const', url: 'https://docs.rs/crate/test_crate/latest/test_crate/module_b/inner_b/trait.InnerBTrait.html#associatedconstant.ASSOCIATED_CONST' },
34
+ { path: 'module_b::inner_b::InnerBTrait::inner_method', kind: 'functiontype', url: 'https://docs.rs/crate/test_crate/latest/test_crate/module_b/inner_b/trait.InnerBTrait.html#tymethod.inner_method' },
35
+ { path: 'module_b::inner_b::InnerBTrait::provided_method', kind: 'function', url: 'https://docs.rs/crate/test_crate/latest/test_crate/module_b/inner_b/trait.InnerBTrait.html#method.provided_method' }
36
+ ],
37
+ 'constants': [
38
+ { path: 'TEST_CONST', kind: 'constant', url: 'https://docs.rs/crate/test_crate/latest/test_crate/constant.TEST_CONST.html' }
39
+ ],
40
+ 'types': [
41
+ { path: 'TestTypeAlias', kind: 'type_alias', url: 'https://docs.rs/crate/test_crate/latest/test_crate/type.TestTypeAlias.html' }
42
+ ],
43
+ 'macros': [
44
+ { path: 'test_macro', kind: 'macro', url: 'https://docs.rs/crate/test_crate/latest/test_crate/macro.test_macro.html' }
45
+ ]
46
+ };
47
+ function testItem(itemPath, expectedKind, expectedUrl) {
48
+ let expectedName = itemPath.includes("::") ? itemPath.split("::").pop() : itemPath;
49
+ const item = crateDocs.get(itemPath);
50
+ expect(item).toBeDefined();
51
+ expect(item.name).toBe(expectedName);
52
+ expect(item.kind).toBe(expectedKind);
53
+ expect(item.url).toBe(expectedUrl);
54
+ }
55
+ function testItems(testCases) {
56
+ for (const testCase of testCases) {
57
+ testItem(testCase.path, testCase.kind, testCase.url);
58
+ }
59
+ }
60
+ test('can get functions', () => testItems(testSet.functions));
61
+ test('can get external items', () => testItems(testSet.external));
62
+ test('can get structures', () => testItems(testSet.structures));
63
+ test('can get enums', () => testItems(testSet.enums));
64
+ test('can get modules', () => testItems(testSet.modules));
65
+ test('can get traits', () => testItems(testSet.traits));
66
+ test('can get constants', () => testItems(testSet.constants));
67
+ test('can get types', () => testItems(testSet.types));
68
+ test('can get macros', () => testItems(testSet.macros));
69
+ // After all tests, verify that the URLs actually exist
70
+ afterAll(() => {
71
+ const source = new DocumentationSource('test/test_crate');
72
+ let result = source.callCargo(['doc', '--no-deps', '--document-private-items', '--lib', '--quiet']);
73
+ expect(result).toBe(0);
74
+ let docPath = source.getDocPath();
75
+ for (const suite in testSet) {
76
+ for (const testCase of testSet[suite]) {
77
+ let url = testCase.url;
78
+ let item = crateDocs.get(testCase.path);
79
+ expect(item).toBeDefined();
80
+ let crate = item.path.split("::")[0];
81
+ if (crate == source.name) {
82
+ // Verify the file exists locally
83
+ let relativePath = url.replace(`https://docs.rs/crate/${source.name}/latest/`, `${docPath}/`);
84
+ let hash = relativePath.split('#')[1] || '';
85
+ if (hash.length > 0) {
86
+ relativePath = relativePath.replace(`#${hash}`, '');
87
+ }
88
+ expect(fs.existsSync(relativePath), `[${suite}] File does not exist: ${relativePath}`).toBe(true);
89
+ // Should contain the hash if applicable
90
+ if (hash.length > 0) {
91
+ let fileContents = fs.readFileSync(relativePath, 'utf-8');
92
+ expect(fileContents.includes(`id="${hash}"`), `[${suite}] File does not contain id="${hash}": ${relativePath}`).toBe(true);
93
+ }
94
+ }
95
+ else {
96
+ // Fetch the URL to ensure it exists remotely
97
+ let url = item.url;
98
+ fetch(url).then(response => {
99
+ expect(response.status, `[${suite}] Failed to fetch ${url}`).toBe(200);
100
+ }).catch(err => {
101
+ throw new Error(`Failed to fetch ${url}: ${err}`);
102
+ });
103
+ }
104
+ }
105
+ }
106
+ });
107
+ //# sourceMappingURL=item.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"item.test.js","sourceRoot":"","sources":["item.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,128 @@
1
+ import { test, expect, afterAll } from 'vitest';
2
+ import fs from 'fs';
3
+ import fetch from 'node-fetch';
4
+
5
+ import { loadCrate } from '../src/lib';
6
+ import { DocumentationSource } from '../src/json';
7
+
8
+ const crateDocs = loadCrate('test/test_crate');
9
+
10
+ let testSet = {
11
+ 'functions': [
12
+ { path: 'add', kind: 'function', url: 'https://docs.rs/crate/test_crate/latest/test_crate/fn.add.html' },
13
+ { path: 'test_crate::add', kind: 'function', url: 'https://docs.rs/crate/test_crate/latest/test_crate/fn.add.html' }
14
+ ],
15
+
16
+ 'external': [
17
+ { path: 'Serialize', kind: 'trait', url: 'https://docs.rs/crate/test_crate/latest/test_crate/trait.Serialize.html' }
18
+ ],
19
+
20
+ 'structures': [
21
+ { path: 'TestStructPlain', kind: 'struct', url: 'https://docs.rs/crate/test_crate/latest/test_crate/struct.TestStructPlain.html' },
22
+ { path: 'TestStructPlain::new', kind: 'function', url: 'https://docs.rs/crate/test_crate/latest/test_crate/struct.TestStructPlain.html#method.new' },
23
+ { path: 'TestStructPlain::field1', kind: 'struct_field', url: 'https://docs.rs/crate/test_crate/latest/test_crate/struct.TestStructPlain.html#structfield.field1' },
24
+ { path: 'TestStructTuple::0', kind: 'struct_field', url: 'https://docs.rs/crate/test_crate/latest/test_crate/struct.TestStructTuple.html#structfield.0' }
25
+ ],
26
+
27
+ 'enums': [
28
+ { path: 'TestEnum', kind: 'enum', url: 'https://docs.rs/crate/test_crate/latest/test_crate/enum.TestEnum.html' },
29
+ { path: 'TestEnum::VariantThree', kind: 'variant', url: 'https://docs.rs/crate/test_crate/latest/test_crate/enum.TestEnum.html#variant.VariantThree' },
30
+ { path: 'TestEnum::describe', kind: 'function', url: 'https://docs.rs/crate/test_crate/latest/test_crate/enum.TestEnum.html#method.describe' }
31
+ ],
32
+
33
+ 'modules': [
34
+ { path: 'module_b', kind: 'module', url: 'https://docs.rs/crate/test_crate/latest/test_crate/module_b/index.html' },
35
+ { path: 'module_b::inner_b', kind: 'module', url: 'https://docs.rs/crate/test_crate/latest/test_crate/module_b/inner_b/index.html' }
36
+ ],
37
+
38
+ 'traits': [
39
+ { path: 'module_b::inner_b::InnerBTrait', kind: 'trait', url: 'https://docs.rs/crate/test_crate/latest/test_crate/module_b/inner_b/trait.InnerBTrait.html' },
40
+ { path: 'module_b::inner_b::InnerBTrait::AssociatedType', kind: 'assoc_type', url: 'https://docs.rs/crate/test_crate/latest/test_crate/module_b/inner_b/trait.InnerBTrait.html#associatedtype.AssociatedType' },
41
+ { path: 'module_b::inner_b::InnerBTrait::ASSOCIATED_CONST', kind: 'assoc_const', url: 'https://docs.rs/crate/test_crate/latest/test_crate/module_b/inner_b/trait.InnerBTrait.html#associatedconstant.ASSOCIATED_CONST' },
42
+ { path: 'module_b::inner_b::InnerBTrait::inner_method', kind: 'functiontype', url: 'https://docs.rs/crate/test_crate/latest/test_crate/module_b/inner_b/trait.InnerBTrait.html#tymethod.inner_method' },
43
+ { path: 'module_b::inner_b::InnerBTrait::provided_method', kind: 'function', url: 'https://docs.rs/crate/test_crate/latest/test_crate/module_b/inner_b/trait.InnerBTrait.html#method.provided_method' }
44
+ ],
45
+
46
+ 'constants': [
47
+ { path: 'TEST_CONST', kind: 'constant', url: 'https://docs.rs/crate/test_crate/latest/test_crate/constant.TEST_CONST.html' }
48
+ ],
49
+
50
+ 'types': [
51
+ { path: 'TestTypeAlias', kind: 'type_alias', url: 'https://docs.rs/crate/test_crate/latest/test_crate/type.TestTypeAlias.html' }
52
+ ],
53
+
54
+ 'macros': [
55
+ { path: 'test_macro', kind: 'macro', url: 'https://docs.rs/crate/test_crate/latest/test_crate/macro.test_macro.html' }
56
+ ]
57
+ }
58
+
59
+ function testItem(itemPath: string, expectedKind: string, expectedUrl: string) {
60
+ let expectedName = itemPath.includes("::") ? itemPath.split("::").pop()! : itemPath;
61
+ const item = crateDocs.get(itemPath);
62
+
63
+ expect(item).toBeDefined();
64
+ expect(item.name).toBe(expectedName);
65
+ expect(item.kind).toBe(expectedKind);
66
+ expect(item.url).toBe(expectedUrl);
67
+ }
68
+
69
+ function testItems(testCases: Array<{ path: string, kind: string, url: string }>) {
70
+ for (const testCase of testCases) {
71
+ testItem(testCase.path, testCase.kind, testCase.url);
72
+ }
73
+ }
74
+
75
+ test('can get functions', () => testItems(testSet.functions));
76
+ test('can get external items', () => testItems(testSet.external));
77
+ test('can get structures', () => testItems(testSet.structures));
78
+ test('can get enums', () => testItems(testSet.enums));
79
+ test('can get modules', () => testItems(testSet.modules));
80
+ test('can get traits', () => testItems(testSet.traits));
81
+ test('can get constants', () => testItems(testSet.constants));
82
+ test('can get types', () => testItems(testSet.types));
83
+ test('can get macros', () => testItems(testSet.macros));
84
+
85
+ // After all tests, verify that the URLs actually exist
86
+ afterAll(async () => {
87
+ const source = new DocumentationSource('test/test_crate');
88
+ let result = source.callCargo(['doc', '--no-deps', '--document-private-items', '--lib', '--quiet']);
89
+ expect(result).toBe(0);
90
+
91
+ let docPath = source.getDocPath();
92
+
93
+ for (const suite in testSet) {
94
+ for (const testCase of (testSet as any)[suite]) {
95
+ let url = testCase.url;
96
+ let item = crateDocs.get(testCase.path);
97
+ expect(item).toBeDefined();
98
+
99
+ let crate = item.path.split("::")[0];
100
+ if (crate == source.name) {
101
+ // Verify the file exists locally
102
+ let relativePath = url.replace(`https://docs.rs/crate/${source.name}/latest/`, `${docPath}/`);
103
+
104
+ let hash = relativePath.split('#')[1] || '';
105
+ if (hash.length > 0) {
106
+ relativePath = relativePath.replace(`#${hash}`, '');
107
+ }
108
+
109
+ expect(fs.existsSync(relativePath), `[${suite}] File does not exist: ${relativePath}`).toBe(true);
110
+
111
+ // Should contain the hash if applicable
112
+ if (hash.length > 0) {
113
+ let fileContents = fs.readFileSync(relativePath, 'utf-8');
114
+ expect(fileContents.includes(`id="${hash}"`), `[${suite}] File does not contain id="${hash}": ${relativePath}`).toBe(true);
115
+ }
116
+
117
+ } else {
118
+ // Fetch the URL to ensure it exists remotely
119
+ let url = item.url;
120
+ await fetch(url).then(response => {
121
+ expect(response.status, `[${suite}] Failed to fetch ${url}`).toBe(200);
122
+ }).catch(err => {
123
+ throw new Error(`Failed to fetch ${url}: ${err}`);
124
+ });
125
+ }
126
+ }
127
+ }
128
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=json.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json.test.d.ts","sourceRoot":"","sources":["json.test.ts"],"names":[],"mappings":""}