Skip to content

iamguid/catfish

Repository files navigation

Catfish

️:warning: Experimental ️:warning:

What is Catfish ?

Catfish is first fully customizable client side (TypeScript) code generation tool for gRPC.

With Catfish you can:

  • Extend every code generation plugin using very friendly, simple and powerfull syntax
  • Write your own code generation plugins without pain
  • Use library without dependencies like protoc, buf or java
  • Use with protoc or buf (in progress)

Catfish packages:

  • Parser - Based on ANTL4 .proto files parser (currently works only with protobuf V3)
  • Generator - Basic classes & types for code generation and many ready to use plugins
  • Runtime - Some logic that you might need in generated fiels

Catfish plugins:

  • protobuf - generates protobuf messages
  • grpc-web - generates grpc-web compatible clients
  • grpc-web-rxjs - generates grpc-web compatible clients based on rxjs library
  • grpc-web-catfish - generates extensions for grpc-web clients that you can use to reduce boilerplate (currently implemented only paginators)
  • grpc-web-tanstack - generates extensions for grpc-web clients that you can use with reqct-query library

How Plugins Works ?

Plugin contains at least 4 main files: plugin.ts, context.ts, templates.ts and index.ts

plugin.ts

plugin.ts - the main entry point of plugin. That file should export only one method plugin and method should return result files.

There is an example of typical plugin structure:

// File name builder function that we will use for output file path
export const fileNameBuilder = (file: FileDescriptor, ctx: ProjectContext) => replaceProtoSuffix(ctx.getProtoFilePath(file), 'list.ts');

// Plugin options
export interface PluginOptions extends BasePluginOptions {}

export const plugin: Plugin<PluginOptions, PluginTemplatesRegistry, PluginContextDefinition> = async (projectContext, projectOptions, pluginOptions, registerTemplates, buildContext) => {
    // Define result variable with result files array
    const result: PluginOutputFile[] = []

    // Define defaults
    const pluginOptions_ = pluginOptions ?? ({} as PluginOptions );
    const registerTemplates_ = registerTemplates ?? registerPluginTemplates
    const buildContext_ = buildContext ?? buildPluginContext

    // Register templates in templates registry
    const templatesRegistry = new TemplatesRegistry<PluginOptions, PluginTemplatesRegistry>(projectContext, pluginOptions_)
    registerTemplates_(templatesRegistry)

    // Get all files descriptors from project context
    const files = projectContext.getFiles();

    // We use await because context build is async operation, it is needed for type resolution
    await Promise.all(files.map(async (file) => {
        // Create context registry and build context
        const contextsRegistry = new ContextsRegistry(projectContext, file, fileNameBuilder(file, projectContext), pluginOptions_)
        const context = buildContext_(contextsRegistry);

        // Capture usages for generate imports, that we will put in top of the file,
        // all usages in context will be captured per file
        const captureContext = projectContext.resolver.getCaptureContext()
        const fileContext = await context.build(captureContext)
        const usedThings = captureContext.stopCapture();

        // Render file template
        const resultFilePath = fileNameBuilder(file, projectContext);
        const imports = projectContext.resolver.getImports(usedThings, file, resultFilePath)
        const messages = extractAllMessages(fileContext);
        const resultFileContent = templatesRegistry.render('main', { file: fileContext, imports, messages });

        // Store result file to output files
        result.push({ path: resultFilePath, content: resultFileContent });
    }))

    // Return result
    return { files: result }
}

// ...

context.ts

context.ts - context is prepared data for templates

There is an example how you can easily extend context:

// Special types to define result context, that you can use in templates
export type PluginContextFlatDefinition = ExtractFlatContextDefinition<ReturnType<typeof buildPluginContext>>;
export type PluginContextDefinition = ExtractContextDefinition<ReturnType<typeof buildPluginContext>>;

// Method for extending context
export const buildPluginContext = (cr: ContextsRegistry<PluginOptions>) => {
    return cr
        // Expose fields thing and path to all messages
        .extend('messages', async ({ ctx, use }) => ({
            ...ctx,
            thing: await use('model.class', ctx.desc),
            path: ctx.desc.fullpath
        }))
}

templates.ts

templates.ts - templates is just templates that plugin use to get file content from context

There is an example of templates:

// Templates types (name and context that will be propogated to template function)
export type PluginTemplatesRegistry = {
  main: { file: PluginContextFlatDefinition['file'], imports: Import[], messages: PluginContextDefinition['messages'][] },
  listType: { messages: PluginContextDefinition['messages'][] },
}

export const registerPluginTemplates: TemplatesBuilder<PluginOptions, PluginTemplatesRegistry> = (t) => {
  // Template function main
  t.register('main', ({ file, imports, messages }) => `
    ${t.renderHeader(file.desc)}

    ${t.renderImports(imports)}

    ${t.render('listType', { messages })} 
  `)

  // Template function listType
  t.register('listType', ({ messages }) => `
    type Messages = {
      ${messages.map(message => `'${message.path}': ${message.thing.usagename}`)}
    }
  `)
}

Everything that you define in context.ts will be propagated to templates context

See full example for more details

About

Easy to use gRPC client code generation tool

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published