diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 000000000..bca4b0893 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,22 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run quicktype:*)", + "Bash(ls:*)", + "Bash(npm run build:*)", + "Bash(node:*)", + "Bash(grep:*)", + "Bash(mkdir:*)", + "Bash(npx:*)", + "Bash(npm link:*)", + "Bash(quicktype:*)", + "Bash(chmod:*)", + "Bash(tsc)", + "Bash(find:*)", + "Bash(cat:*)", + "Bash(DEBUG=1 quicktype test-snake-case.json --lang kotlin --framework jackson --mongo-collection asset_github_repository)", + "Bash(DEBUG=1 node dist/index.js test-snake-case.json --lang kotlin --framework jackson --mongo-collection asset_github_repository)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/packages/quicktype-core/src/ConvenienceRenderer.ts b/packages/quicktype-core/src/ConvenienceRenderer.ts index 41ae7e4dd..0dc054a95 100644 --- a/packages/quicktype-core/src/ConvenienceRenderer.ts +++ b/packages/quicktype-core/src/ConvenienceRenderer.ts @@ -77,6 +77,7 @@ import { nullableFromUnion, separateNamedTypes, } from "./Type/TypeUtils"; +import { getPluginRunner, type PluginRunner } from "./PluginSupport"; const wordWrap: (s: string) => string = _wordwrap(90); @@ -170,17 +171,31 @@ export abstract class ConvenienceRenderer extends Renderer { private _cycleBreakerTypes?: Set | undefined; private _alphabetizeProperties = false; + + protected pluginRunner: PluginRunner | undefined; public constructor( targetLanguage: TargetLanguage, renderContext: RenderContext, ) { super(targetLanguage, renderContext); + // Plugin runner will be initialized by subclasses that pass options } public get topLevels(): ReadonlyMap { return this.typeGraph.topLevels; } + + // Plugin hook methods - to be called by language renderers + protected runPluginHook(hookName: string, context?: any): void { + if (this.pluginRunner) { + this.pluginRunner.runHook(hookName, context); + } + } + + protected initializePlugins(options: any): void { + this.pluginRunner = getPluginRunner(this, options); + } /** * Return an array of strings which are not allowed as names in the global diff --git a/packages/quicktype-core/src/PluginSupport.ts b/packages/quicktype-core/src/PluginSupport.ts new file mode 100644 index 000000000..3b2046d1c --- /dev/null +++ b/packages/quicktype-core/src/PluginSupport.ts @@ -0,0 +1,41 @@ +// Plugin support stub for quicktype-core +// This allows the core package to support plugins without circular dependencies + +export interface PluginContext { + options: Record; + language: string; + framework?: string; + emitLine: (code: any) => void; + emitAnnotation: (annotation: string) => void; +} + +export interface PluginRunner { + runHook(hookName: string, context?: any): void; +} + +// Global plugin runner instance - will be set by the main quicktype package +export let globalPluginRunner: PluginRunner | undefined; + +export function setGlobalPluginRunner(runner: PluginRunner): void { + globalPluginRunner = runner; +} + +export function getPluginRunner(renderer: any, options: any): PluginRunner | undefined { + if (!globalPluginRunner) return undefined; + + // Create a context-specific runner + return { + runHook(hookName: string, context?: any) { + const fullContext = { + renderer, + options, + language: renderer.targetLanguage.name, + framework: options.framework, + emitLine: (code: any) => renderer.emitLine(code), + emitAnnotation: (annotation: string) => renderer.emitLine(annotation), + ...context + }; + globalPluginRunner!.runHook(hookName, fullContext); + } + }; +} \ No newline at end of file diff --git a/packages/quicktype-core/src/index.ts b/packages/quicktype-core/src/index.ts index 840052701..4f7000cb6 100644 --- a/packages/quicktype-core/src/index.ts +++ b/packages/quicktype-core/src/index.ts @@ -40,6 +40,7 @@ export { type OptionValues, } from "./RendererOptions"; export { TargetLanguage, type MultiFileRenderResult } from "./TargetLanguage"; +export { setGlobalPluginRunner, type PluginRunner } from "./PluginSupport"; export { type MultiWord, diff --git a/packages/quicktype-core/src/language/Java/JavaJacksonRenderer.ts b/packages/quicktype-core/src/language/Java/JavaJacksonRenderer.ts index 721bada43..ccb3a8a6b 100644 --- a/packages/quicktype-core/src/language/Java/JavaJacksonRenderer.ts +++ b/packages/quicktype-core/src/language/Java/JavaJacksonRenderer.ts @@ -89,8 +89,28 @@ export class JacksonRenderer extends JavaRenderer { default: break; } + + // Run plugin hook for property annotations + const propertyAnnotations: string[] = []; + const originalEmitLine = this.emitLine.bind(this); + + // Temporarily override emitLine to capture annotations + (this as any).emitLine = (line: string) => { + propertyAnnotations.push(line); + }; + (this as any).emitAnnotation = (line: string) => { + propertyAnnotations.push(line); + }; + + this.runPluginHook('beforeProperty', { + propertyName: this.sourcelikeToString(_propertyName), + jsonName: jsonName + }); + + // Restore original emitLine + (this as any).emitLine = originalEmitLine; - return [...superAnnotations, ...annotations]; + return [...superAnnotations, ...annotations, ...propertyAnnotations]; } protected importsForType(t: ClassType | UnionType | EnumType): string[] { diff --git a/packages/quicktype-core/src/language/Java/JavaRenderer.ts b/packages/quicktype-core/src/language/Java/JavaRenderer.ts index 5d24de999..254a2552f 100644 --- a/packages/quicktype-core/src/language/Java/JavaRenderer.ts +++ b/packages/quicktype-core/src/language/Java/JavaRenderer.ts @@ -66,6 +66,9 @@ export class JavaRenderer extends ConvenienceRenderer { protected readonly _options: OptionValues, ) { super(targetLanguage, renderContext); + + // Initialize plugin runner + this.initializePlugins(_options); switch (_options.dateTimeProvider) { case "legacy": @@ -265,6 +268,9 @@ export class JavaRenderer extends ConvenienceRenderer { for (const pkg of imports) { this.emitLine("import ", pkg, ";"); } + + // Run plugin hook for additional imports + this.runPluginHook('afterImports'); } protected emitFileHeader(fileName: Sourcelike, imports: string[]): void { @@ -439,10 +445,16 @@ export class JavaRenderer extends ConvenienceRenderer { return this.javaType(reference, t); } - protected emitClassAttributes(_c: ClassType, _className: Name): void { + protected emitClassAttributes(c: ClassType, className: Name): void { if (this._options.lombok) { this.emitLine("@lombok.Data"); } + + // Run plugin hook for class annotations + this.runPluginHook('beforeClass', { + classType: c, + className: className + }); } protected annotationsForAccessor( diff --git a/packages/quicktype-core/src/language/Kotlin/KotlinJacksonRenderer.ts b/packages/quicktype-core/src/language/Kotlin/KotlinJacksonRenderer.ts index 3401ed1fe..8199700e6 100644 --- a/packages/quicktype-core/src/language/Kotlin/KotlinJacksonRenderer.ts +++ b/packages/quicktype-core/src/language/Kotlin/KotlinJacksonRenderer.ts @@ -21,7 +21,6 @@ import { matchType, nullableFromUnion } from "../../Type/TypeUtils"; import { KotlinRenderer } from "./KotlinRenderer"; import type { kotlinOptions } from "./language"; import { stringEscape } from "./utils"; - export class KotlinJacksonRenderer extends KotlinRenderer { public constructor( targetLanguage: TargetLanguage, @@ -79,6 +78,9 @@ import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.node.* import com.fasterxml.jackson.databind.ser.std.StdSerializer import com.fasterxml.jackson.module.kotlin.*`); + + // Call emitImports to run plugin hooks + this.emitImports(); const hasUnions = iterableSome( this.typeGraph.allNamedTypes(), @@ -260,6 +262,14 @@ import com.fasterxml.jackson.module.kotlin.*`); if (rename !== undefined) { meta.push(() => this.emitLine(rename)); } + + // Call parent class to handle plugin hooks + super.renameAttribute(name, jsonName, required, meta); + } + + protected emitClassAnnotations(c: Type, className: Name): void { + // Call parent class to handle plugin hooks + super.emitClassAnnotations(c, className); } protected emitEnumDefinition(e: EnumType, enumName: Name): void { diff --git a/packages/quicktype-core/src/language/Kotlin/KotlinRenderer.ts b/packages/quicktype-core/src/language/Kotlin/KotlinRenderer.ts index 5e1c45b6b..07b314e8b 100644 --- a/packages/quicktype-core/src/language/Kotlin/KotlinRenderer.ts +++ b/packages/quicktype-core/src/language/Kotlin/KotlinRenderer.ts @@ -40,6 +40,9 @@ export class KotlinRenderer extends ConvenienceRenderer { protected readonly _kotlinOptions: OptionValues, ) { super(targetLanguage, renderContext); + + // Initialize plugin runner + this.initializePlugins(_kotlinOptions); } protected forbiddenNamesForGlobalNamespace(): readonly string[] { @@ -212,6 +215,12 @@ export class KotlinRenderer extends ConvenienceRenderer { this.emitLine("package ", this._kotlinOptions.packageName); this.ensureBlankLine(); } + + protected emitImports(): void { + // Base implementation - framework renderers can override + // Run plugin hook for additional imports + this.runPluginHook('afterImports'); + } protected emitTopLevelPrimitive(t: PrimitiveType, name: Name): void { const elementType = this.kotlinType(t); @@ -314,17 +323,46 @@ export class KotlinRenderer extends ConvenienceRenderer { this.emitLine(")"); } - protected emitClassAnnotations(_c: Type, _className: Name): void { - // to be overridden + protected emitClassAnnotations(c: Type, className: Name): void { + // Run plugin hook for class annotations + this.runPluginHook('beforeClass', { + classType: c, + className: className + }); } protected renameAttribute( - _name: Name, - _jsonName: string, + name: Name, + jsonName: string, _required: boolean, - _meta: Array<() => void>, + meta: Array<() => void>, ): void { - // to be overridden + // Run plugin hook for property annotations + const propertyAnnotations: string[] = []; + const originalEmitLine = this.emitLine.bind(this); + + // Temporarily override emitLine to capture annotations + (this as any).emitLine = (line: string) => { + propertyAnnotations.push(line); + }; + (this as any).emitAnnotation = (line: string) => { + propertyAnnotations.push(line); + }; + + this.runPluginHook('beforeProperty', { + propertyName: this.sourcelikeToString(name), + jsonName: jsonName + }); + + // Restore original emitLine + (this as any).emitLine = originalEmitLine; + + // Add captured annotations to meta + if (propertyAnnotations.length > 0) { + meta.push(() => { + propertyAnnotations.forEach(line => this.emitLine(line)); + }); + } } protected emitEnumDefinition(e: EnumType, enumName: Name): void { diff --git a/packages/quicktype-core/src/language/Kotlin/language.ts b/packages/quicktype-core/src/language/Kotlin/language.ts index ce72896ac..aa6984fd0 100644 --- a/packages/quicktype-core/src/language/Kotlin/language.ts +++ b/packages/quicktype-core/src/language/Kotlin/language.ts @@ -61,16 +61,19 @@ export class KotlinTargetLanguage extends TargetLanguage< untypedOptionValues: RendererOptions, ): ConvenienceRenderer { const options = getOptionValues(kotlinOptions, untypedOptionValues); + + // Merge in any additional options from plugins (like mongo-collection) + const allOptions = { ...options, ...untypedOptionValues }; switch (options.framework) { case "None": - return new KotlinRenderer(this, renderContext, options); + return new KotlinRenderer(this, renderContext, allOptions); case "Jackson": - return new KotlinJacksonRenderer(this, renderContext, options); + return new KotlinJacksonRenderer(this, renderContext, allOptions); case "Klaxon": - return new KotlinKlaxonRenderer(this, renderContext, options); + return new KotlinKlaxonRenderer(this, renderContext, allOptions); case "KotlinX": - return new KotlinXRenderer(this, renderContext, options); + return new KotlinXRenderer(this, renderContext, allOptions); default: return assertNever(options.framework); } diff --git a/src/index.ts b/src/index.ts index 856542c96..50360bc2a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -66,6 +66,12 @@ import type { TypeSource, } from "./TypeSource"; import { urlsFromURLGrammar } from "./URLGrammar"; +import { pluginRegistry } from "./plugins/registry"; +import { globalPluginRunner } from "./plugins/PluginRunner"; +import { setGlobalPluginRunner } from "quicktype-core"; + +// Set up global plugin runner +setGlobalPluginRunner(globalPluginRunner); // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires const packageJSON = require("../package.json"); @@ -101,6 +107,7 @@ export interface CLIOptions { topLevel: string; version: boolean; + plugins?: string[]; } const defaultDefaultTargetLanguageName = "go"; @@ -370,6 +377,7 @@ function inferCLIOptions( httpHeader: opts.httpHeader, debug: opts.debug, telemetry: opts.telemetry, + plugins: opts.plugins, }; for (const flagName of inferenceFlagNames) { const cliName = negatedInferenceFlagName(flagName); @@ -407,6 +415,9 @@ function dashedFromCamelCase(name: string): string { function makeOptionDefinitions( targetLanguages: readonly TargetLanguage[], ): OptionDefinition[] { + // Allow plugins to modify option definitions + const languageName = targetLanguages.length === 1 ? targetLanguages[0].name : "typescript"; + let allDefinitions: OptionDefinition[] = []; const beforeLang: OptionDefinition[] = [ { name: "out", @@ -562,6 +573,15 @@ function makeOptionDefinitions( description: "Enable anonymous telemetry to help improve quicktype", kind: "cli", }, + { + name: "plugins", + alias: "p", + optionType: "string", + multiple: true, + typeLabel: "PLUGIN", + description: "Enable plugins (e.g., mongodb, validation)", + kind: "cli", + }, { name: "help", alias: "h", @@ -577,7 +597,12 @@ function makeOptionDefinitions( kind: "cli", }, ]; - return beforeLang.concat(lang, afterLang, inference, afterInference); + allDefinitions = beforeLang.concat(lang, afterLang, inference, afterInference); + + // Let plugins modify option definitions + allDefinitions = pluginRegistry.modifyOptions(allDefinitions, languageName); + + return allDefinitions; } interface ColumnDefinition { @@ -1214,6 +1239,7 @@ export function writeOutput( export async function main( args: string[] | Partial, ): Promise { + console.log("quicktype CLI is running! v3"); let cliOptions: CLIOptions; if (Array.isArray(args)) { cliOptions = parseCLIOptions(args); @@ -1240,6 +1266,18 @@ export async function main( } } + // Enable plugins if specified + if (cliOptions.plugins) { + for (const pluginName of cliOptions.plugins) { + try { + pluginRegistry.enablePlugin(pluginName); + } catch (e) { + console.error(chalk.red(`Failed to enable plugin "${pluginName}": ${e}`)); + return; + } + } + } + const quicktypeOptions = await makeQuicktypeOptions(cliOptions); if (quicktypeOptions === undefined) { return; @@ -1254,6 +1292,9 @@ if (require.main === module) { main(process.argv.slice(2)).catch((e) => { if (e instanceof Error) { console.error(`Error: ${e.message}.`); + if (process.env.DEBUG) { + console.error(e.stack); + } } else { console.error(e); } diff --git a/src/plugins/PluginInterface.ts b/src/plugins/PluginInterface.ts new file mode 100644 index 000000000..acec6b0bd --- /dev/null +++ b/src/plugins/PluginInterface.ts @@ -0,0 +1,69 @@ +import type { Name, Type, OptionDefinition, Sourcelike } from "quicktype-core"; + +export interface PluginContext { + language: string; + framework?: string; + options: Record; + + // Current generation context + currentClass?: { + name: Name; + type: Type; + isTopLevel: boolean; + }; + + currentProperty?: { + name: Name; + jsonName: string; + type: Type; + required: boolean; + }; + + // Methods to emit code + emit: (code: Sourcelike) => void; + emitLine: (code: Sourcelike) => void; + emitAnnotation: (annotation: string) => void; +} + +export interface PluginHooks { + // Modify CLI options + beforeOptions?: (options: OptionDefinition[]) => OptionDefinition[]; + + // Modify imports + afterImports?: (context: PluginContext) => void; + + // Add class annotations + beforeClass?: (context: PluginContext) => void; + afterClass?: (context: PluginContext) => void; + + // Add property annotations + beforeProperty?: (context: PluginContext) => void; + afterProperty?: (context: PluginContext) => void; + + // Transform the final output + transformOutput?: (output: string, context: PluginContext) => string; +} + +export interface QuicktypePlugin { + name: string; + version: string; + description: string; + + // Which languages/frameworks this plugin supports + supports: { + languages?: string[]; + frameworks?: string[]; + }; + + // Plugin hooks + hooks: PluginHooks; +} + +export interface PluginMetadata { + name: string; + version: string; + description: string; + author?: string; + repository?: string; + main?: string; +} \ No newline at end of file diff --git a/src/plugins/PluginRunner.ts b/src/plugins/PluginRunner.ts new file mode 100644 index 000000000..1d08028ed --- /dev/null +++ b/src/plugins/PluginRunner.ts @@ -0,0 +1,54 @@ +import { pluginRegistry } from "./registry"; +import type { PluginContext } from "./PluginInterface"; + +export class GlobalPluginRunner { + runHook(hookName: string, fullContext: any): void { + const { renderer, options, ...hookContext } = fullContext; + + const language = fullContext.language || (renderer?.targetLanguage?.name); + const framework = fullContext.framework || options?.framework; + + const enabledPlugins = pluginRegistry.getEnabledPlugins(language, framework); + + // Create context for this hook + const context: PluginContext = { + options, + language, + framework, + emit: fullContext.emitLine, + emitLine: fullContext.emitLine, + emitAnnotation: fullContext.emitAnnotation, + }; + + // Special handling for class context + if (hookContext.classType && renderer?.topLevels) { + const isTopLevel = Array.from(renderer.topLevels.values()).includes(hookContext.classType); + context.currentClass = { + name: hookContext.className, + type: hookContext.classType, + isTopLevel + }; + } + + // Special handling for property context + if (hookContext.propertyName !== undefined && hookContext.jsonName !== undefined) { + context.currentProperty = { + name: hookContext.propertyName as any, + jsonName: hookContext.jsonName, + type: null as any, + required: true + }; + } + + for (const plugin of enabledPlugins) { + // Run the hook if it exists + const hook = (plugin.hooks as any)[hookName]; + if (typeof hook === 'function') { + hook(context); + } + } + } +} + +// Global instance +export const globalPluginRunner = new GlobalPluginRunner(); \ No newline at end of file diff --git a/src/plugins/builtin/mongodb/index.ts b/src/plugins/builtin/mongodb/index.ts new file mode 100644 index 000000000..136ab21ca --- /dev/null +++ b/src/plugins/builtin/mongodb/index.ts @@ -0,0 +1,57 @@ +import type { QuicktypePlugin, PluginContext } from "../../PluginInterface"; +import type { OptionDefinition } from "quicktype-core"; + +class MongoDBPlugin implements QuicktypePlugin { + name = "mongodb"; + version = "1.0.0"; + description = "Add MongoDB annotations for Spring Data"; + + supports = { + languages: ["kotlin", "java"] + // Don't check frameworks - Java uses Jackson by default, Kotlin requires --framework jackson + }; + + hooks = { + beforeOptions: (options: OptionDefinition[]) => { + return [...options, { + name: "mongo-collection", + alias: "m", + optionType: "string" as const, + typeLabel: "COLLECTION", + description: "MongoDB collection name for @Document annotation", + } as OptionDefinition]; + }, + + afterImports: (context: PluginContext) => { + if (context.options["mongo-collection"]) { + context.emitLine(""); + if (context.language === "java") { + context.emitLine("import org.springframework.data.mongodb.core.mapping.Document;"); + context.emitLine("import org.springframework.data.mongodb.core.mapping.Field;"); + } else if (context.language === "kotlin") { + context.emitLine("import org.springframework.data.mongodb.core.mapping.Document"); + context.emitLine("import org.springframework.data.mongodb.core.mapping.Field"); + } + } + }, + + beforeClass: (context: PluginContext) => { + const mongoCollection = context.options["mongo-collection"]; + if (mongoCollection && context.currentClass?.isTopLevel) { + context.emitAnnotation(`@Document("${mongoCollection}")`); + } + }, + + beforeProperty: (context: PluginContext) => { + if (context.options["mongo-collection"] && context.currentProperty) { + // Only add @Field if the property name differs from the JSON name + const propName = String(context.currentProperty.name); + if (propName !== context.currentProperty.jsonName) { + context.emitAnnotation(`@Field("${context.currentProperty.jsonName}")`); + } + } + } + }; +} + +export default new MongoDBPlugin(); \ No newline at end of file diff --git a/src/plugins/builtin/mongodb/plugin.json b/src/plugins/builtin/mongodb/plugin.json new file mode 100644 index 000000000..09e5d2bf1 --- /dev/null +++ b/src/plugins/builtin/mongodb/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "mongodb", + "version": "1.0.0", + "description": "Add MongoDB annotations for Spring Data", + "author": "quicktype", + "main": "index.ts", + "keywords": ["mongodb", "spring-data", "kotlin", "java"] +} \ No newline at end of file diff --git a/src/plugins/registry.ts b/src/plugins/registry.ts new file mode 100644 index 000000000..ab8225a58 --- /dev/null +++ b/src/plugins/registry.ts @@ -0,0 +1,93 @@ +import type { QuicktypePlugin, PluginContext, PluginHooks } from "./PluginInterface"; +import type { OptionDefinition } from "quicktype-core"; +import mongoDBPlugin from "./builtin/mongodb"; + +export class PluginRegistry { + private plugins: Map = new Map(); + private enabledPlugins: Set = new Set(); + + constructor() { + // Register built-in plugins + this.registerPlugin(mongoDBPlugin); + + // Enable MongoDB plugin by default for now + this.enablePlugin("mongodb"); + } + + registerPlugin(plugin: QuicktypePlugin): void { + this.plugins.set(plugin.name, plugin); + } + + enablePlugin(name: string): void { + if (this.plugins.has(name)) { + this.enabledPlugins.add(name); + } else { + throw new Error(`Plugin "${name}" not found`); + } + } + + disablePlugin(name: string): void { + this.enabledPlugins.delete(name); + } + + getPlugin(name: string): QuicktypePlugin | undefined { + return this.plugins.get(name); + } + + getEnabledPlugins(language: string, framework?: string): QuicktypePlugin[] { + const enabled: QuicktypePlugin[] = []; + + for (const name of this.enabledPlugins) { + const plugin = this.plugins.get(name); + if (!plugin) continue; + + // Check if plugin supports this language/framework + const supportsLanguage = !plugin.supports.languages || + plugin.supports.languages.includes(language); + const supportsFramework = !plugin.supports.frameworks || + plugin.supports.frameworks.includes(framework as any); + + if (supportsLanguage && supportsFramework) { + enabled.push(plugin); + } + } + + return enabled; + } + + // Execute hooks for all enabled plugins + executeHook( + hookName: K, + context: PluginContext, + ...args: Parameters> + ): void { + const plugins = this.getEnabledPlugins(context.language, context.framework); + + for (const plugin of plugins) { + const hook = plugin.hooks[hookName]; + if (hook) { + (hook as any).apply(null, args); + } + } + } + + // Special handling for beforeOptions hook - always include all plugin options + // so they can be parsed from CLI + modifyOptions(options: OptionDefinition[], _language: string): OptionDefinition[] { + let modifiedOptions = [...options]; + + // Load all plugins for option definitions + // For CLI parsing, we need to include all plugin options regardless of language + for (const plugin of this.plugins.values()) { + if (plugin.hooks.beforeOptions) { + // Always add options for CLI parsing + modifiedOptions = plugin.hooks.beforeOptions(modifiedOptions); + } + } + + return modifiedOptions; + } +} + +// Global plugin registry instance +export const pluginRegistry = new PluginRegistry(); \ No newline at end of file diff --git a/test-nested.json b/test-nested.json new file mode 100644 index 000000000..82141f6ee --- /dev/null +++ b/test-nested.json @@ -0,0 +1,15 @@ +{ + "id": "123", + "name": "Test Repository", + "url": "https://github.com/test/repo", + "stars": 42, + "owner": { + "id": "456", + "name": "John Doe", + "email": "john@example.com" + }, + "settings": { + "isPrivate": false, + "allowForks": true + } +} \ No newline at end of file diff --git a/test-snake-case.json b/test-snake-case.json new file mode 100644 index 000000000..875819ffb --- /dev/null +++ b/test-snake-case.json @@ -0,0 +1,16 @@ +{ + "repository_id": "123", + "repository_name": "Test Repository", + "html_url": "https://github.com/test/repo", + "star_count": 42, + "repository_owner": { + "user_id": "456", + "full_name": "John Doe", + "email_address": "john@example.com" + }, + "repository_settings": { + "is_private": false, + "allow_forks": true, + "default_branch": "main" + } +} \ No newline at end of file diff --git a/test.json b/test.json new file mode 100644 index 000000000..834622364 --- /dev/null +++ b/test.json @@ -0,0 +1,6 @@ +{ + "id": "123", + "name": "Test Repository", + "url": "https://github.com/test/repo", + "stars": 42 +} \ No newline at end of file