GithubHelp home page GithubHelp logo

go-kod / kod Goto Github PK

View Code? Open in Web Editor NEW
127.0 4.0 3.0 1.14 MB

A generics based dependency injection application framework for Go, support OpenTelemetry trace/metric/log natively ๐Ÿš€๐Ÿš€๐Ÿš€

Home Page: https://pkg.go.dev/github.com/go-kod/kod

License: Apache License 2.0

Go 100.00%
code-generation dependency-injection framework generics go golang interceptor microservice monorepo reflection

kod's Introduction

Kod

Kod stands for Killer Of Dependency, a generics based dependency injection framework for Go.

Although it seems that most Go enthusiasts dislike dependency injection, many companies that widely use Go for their development projects have open-sourced their own dependency injection frameworks. For example, Google has open-sourced Wire, Uber has open-sourced Fx, and Facebook has open-sourced Inject. This is truly a strange phenomenon.

kod

Feature

  • Component Based: Kod is a component-based framework. Components are the building blocks of a Kod application.
  • Configurable: Kod can use TOML/YAML/JSON files to configure how applications are run.
  • Testing: Kod includes a Test function that you can use to test your Kod applications.
  • Logging: Kod provides a logging API, kod.L. Kod also integrates the logs into the environment where your application is deployed.
  • OpenTelemetry: Kod relies on OpenTelemetry to collect trace and metrics from your application.
  • Hooks: Kod provides a way to run code when a component start or stop.
  • Interceptors: Kod has built-in common interceptors, and components can implement the following methods to inject these interceptors into component methods.
  • Interface Generation: Kod provides a way to generate interface from structure.
  • Code Generation: Kod provides a way to generate kod related codes for your kod application.

Installation

go install github.com/go-kod/kod/cmd/kod@latest

If the installation was successful, you should be able to run kod -h:

A powerful tool for writing kod applications.

Usage:
  kod [flags]
  kod [command]

Available Commands:
  callgraph        generate kod callgraph for your kod application.
  completion       Generate the autocompletion script for the specified shell
  generate         generate kod related codes for your kod application.
  help             Help about any command
  struct2interface generate interface from struct for your kod application.

Flags:
  -h, --help      help for kod
  -t, --toggle    Help message for toggle
  -v, --version   Help message for toggle

Use "kod [command] --help" for more information about a command.

Step by Step Tutorial

In this section, we show you how to write Kod applications. To install Kod and follow along, refer to the Installation section. The full source code presented in this tutorial can be found here.

Components

Kod's core abstraction is the component. A component is like an actor, and a Kod application is implemented as a set of components. Concretely, a component is represented with a regular Go interface, and components interact with each other by calling the methods defined by these interfaces.

In this section, we'll define a simple hello component that just prints a string and returns. First, run go mod init hello to create a go module.

mkdir hello/
cd hello/
go mod init hello

Then, create a file called main.go with the following contents:

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/go-kod/kod"
)

func main() {
    if err := kod.Run(context.Background(), serve); err != nil {
        log.Fatal(err)
    }
}

// app is the main component of the application. kod.Run creates
// it and passes it to serve.
type app struct{
    kod.Implements[kod.Main]
}

// serve is called by kod.Run and contains the body of the application.
func serve(context.Context, *app) error {
    fmt.Println("Hello")
    return nil
}

kod.Run(...) initializes and runs the Kod application. In particular, kod.Run finds the main component, creates it, and passes it to a supplied function. In this example, app is the main component since it contains a kod.Implements[kod.Main] field.

go mod tidy
kod generate .
go run .
Hello

FUNDAMENTALS

Components

Components are Kod's core abstraction. Concretely, a component is represented as a Go interface and corresponding implementation of that interface. Consider the following Adder component for example:

type Adder interface {
    Add(context.Context, int, int) (int, error)
}

type adder struct {
    kod.Implements[Adder]
}

func (*adder) Add(_ context.Context, x, y int) (int, error) {
    return x + y, nil
}

Adder defines the component's interface, and adder defines the component's implementation. The two are linked with the embedded kod.Implements[Adder] field. You can call kod.Ref[Adder].Get() to get a caller to the Adder component.

Implementation

A component implementation must be a struct that looks like:

type foo struct{
    kod.Implements[Foo]
    // ...
}

It must be a struct. It must embed a kod.Implements[T] field where T is the component interface it implements. If a component implementation implements an Init(context.Context) error method, it will be called when an instance of the component is created.

func (f *foo) Init(context.Context) error {
    // ...
}

func (f *foo) Shutdown(context.Context) error {
    // ...
}

Lazy Initialization

Components can be lazily initialized by embedding a kod.LazyInit field in the component implementation, which will be initialized when the component is first used, instead of when the application starts.

Simple demo below:

type foo struct {
    kod.Implements[Foo]
    kod.LazyInit
}

Interceptors

Kod has built-in common interceptors, and components can implement the following methods to inject these interceptors into component methods:

func (f *foo) Interceptors() []interceptor.Interceptor {
    return []interceptor.Interceptor{
        kmetric.New(),
        ktrace.New(),
    }
}

Interfaces

Interface can be generated automatically by kod tool.

//go:generate kod struct2interface .

Config

WithConfig

Kod uses config files, written in TOML, to configure how applications are run. A minimal config file, for example, simply lists the application name:

[kod]
name = "hello"

A config file may also contain component-specific configuration sections, which allow you configuring the components in your application. For example, consider the following Greeter component.

type Greeter interface {
    Greet(context.Context, string) (string, error)
}

type greeter struct {
    kod.Implements[Greeter]
}

func (g *greeter) Greet(_ context.Context, name string) (string, error) {
    return fmt.Sprintf("Hello, %s!", name), nil
}

Rather than hard-coding the greeting "Hello", we can provide a greeting in a config file. First, we define an options struct.

type greeterOptions struct {
    Greeting string
}

Next, we associate the options struct with the greeter implementation by embedding the kod.WithConfig[T] struct.

type greeter struct {
    kod.Implements[Greeter]
    kod.WithConfig[greeterOptions]
}

Now, we can add a Greeter section to the config file. The section is keyed by the full path-prefixed name of the component.

["example.com/mypkg/Greeter"]
Greeting = "Bonjour"

When the Greeter component is created, Kod will automatically parse the Greeter section of the config file into a greeterOptions struct. You can access the populated struct via the Config method of the embedded WithConfig struct. For example:

func (g *greeter) Greet(_ context.Context, name string) (string, error) {
    greeting := g.Config().Greeting
    if greeting == "" {
        greeting = "Hello"
    }
    return fmt.Sprintf("%s, %s!", greeting, name), nil
}

You can use TOML struct tags to specify the name that should be used for a field in a config file. For example, we can change the greeterOptions struct to the following.

type greeterOptions struct {
    Greeting string `toml:"my_custom_name"`
}

WithGlobalConfig

Also, we can use the kod.WithGlobalConfig struct to read the whole config from the config file.

type greeter struct {
    kod.Implements[Greeter]
    kod.WithGlobalConfig[greeterOptions]
}

Now, we can add global configuration to the config file based on the greeterOptions struct.

greeting = "Bonjour"

Testing

Unit Test

Kod includes a Test function that you can use to test your Kod applications. For example, create an adder_test.go file with the following contents.

package main

import (
    "context"
    "testing"

    "github.com/go-kod/kod"
)

func TestAdd(t *testing.T) {
     kod.RunTest(t, func(ctx context.Context, adder Adder) {
         got, err := adder.Add(ctx, 1, 2)
         if err != nil {
             t.Fatal(err)
         }
         if want := 3; got != want {
             t.Fatalf("got %q, want %q", got, want)
         }
     })
}

Run go test to run the test. kod.RunTest will create a sub-test and within it will create an Adder component and pass it to the supplied function. If you want to test the implementation of a component, rather than its interface, specify a pointer to the implementing struct as an argument. For example, if the adderImpl struct implemented the Adder interface, we could write the following:

kod.RunTest(t, func(ctx context.Context, adder *adderImpl) {
    // Test adder...
})

Benchmark

You can also use kod.RunTest to benchmark your application. For example, create an adder_benchmark.go file with the following contents.

package main

import (
    "context"
    "testing"

    "github.com/go-kod/kod"
)

func BenchmarkAdd(b *testing.B) {
    kod.RunTest(b, func(ctx context.Context, adder Adder) {
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
            _, err := adder.Add(ctx, 1, 2)
            if err != nil {
                b.Fatal(err)
            }
        }
    })
}

Fake

You can replace a component implementation with a fake implementation in a test using kod.Fake. Here's an example where we replace the real implementation of a Clock component with a fake implementation that always returns a fixed time.

// fakeClock is a fake implementation of the Clock component.
type fakeClock struct {
    now int64
}

// Now implements the Clock component interface. It returns the current time, in
// microseconds, since the unix epoch.
func (f *fakeClock) Now(context.Context) (int64, error) {
    return f.now, nil
}

func TestClock(t *testing.T) {
    t.Run("fake", func(t *testing.T) {
        // Register a fake Clock implementation with the runner.
        fake := kod.Fake[Clock](&fakeClock{100})

        // When a fake is registered for a component, all instances of that
        // component dispatch to the fake.
        kod.RunTest(t, func(ctx context.Context, clock Clock) {
            now, err := clock.UnixMicro(ctx)
            if err != nil {
                t.Fatal(err)
            }
            if now != 100 {
                t.Fatalf("bad time: got %d, want %d", now, 100)
            }

            fake.now = 200
            now, err = clock.UnixMicro(ctx)
            if err != nil {
                t.Fatal(err)
            }
            if now != 200 {
                t.Fatalf("bad time: got %d, want %d", now, 200)
            }
        }, kod.WithFakes(fake))
    })
}

Config

You can also provide the contents of a config file to a runner by setting the Runner.Config field:

func TestArithmetic(t *testing.T) {
    kod.RunTest(t, func(ctx context.Context, adder Adder) {
        // ...
    }, kod.WithConfigFile("testdata/config.toml"))
}

Logging

Kod provides a logging API, kod.L. Kod also integrates the logs into the environment where your application is deployed.

Use the Logger method of a component implementation to get a logger scoped to the component. For example:

type Adder interface {
    Add(context.Context, int, int) (int, error)
}

type adder struct {
    kod.Implements[Adder]
}

func (a *adder) Add(ctx context.Context, x, y int) (int, error) {
    // adder embeds kod.Implements[Adder] which provides the L method.
    logger := a.L(ctx)
    logger.DebugContext(ctx, "A debug log.")
    logger.InfoContext(ctx, "An info log.")
    logger.ErrorContext(ctx, "An error log.", fmt.Errorf("an error"))
    return x + y, nil
}

OpenTelemetry

Kod relies on OpenTelemetry to collect trace and metrics from your application.

Supported Environment Variables:

  • OTEL_SDK_DISABLED: If set to true, disables the OpenTelemetry SDK. Default is false.
  • OTEL_LOGS_EXPORTER: The logs exporter to use. Supported values are "console" and "otlp", Default is "otlp".
  • OTEL_EXPORTER_OTLP_PROTOCOL: The protocol to use for the OTLP exporter. Supported values are "grpc" and "http/protobuf", Default is "http/protobuf".
  • OTEL_EXPORTER_OTLP_INSECURE: If set to true, disables the security features of the OTLP exporter. Default is false.

More information can be found at OpenTelemetry Website.

Acknowledge

This project was heavily inspired by ServiceWeaver.

Star History

Star History Chart

kod's People

Contributors

ccoveille avatar dependabot[bot] avatar sysulq avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

kod's Issues

task `generate:struct2interface` is invalid

$ task generate:struct2interface
task: Task "install:mockgen" is up to date
task: [generate:struct2interface] go run ./cmd/kod struct2interface .
2024/06/08 08:25:25 Loading input failed: tests/case1/kod_gen_interface.go:46:7: failed parsing arguments: tests/case1/kod_gen_interface.go:46:10: unknown package "echo"
panic: mockgen error: exit status 1

goroutine 1 [running]:
github.com/samber/lo.must({0x7b1d80, 0xc0004983c0}, {0x0, 0x0, 0x0})
	/home/whatever/go/pkg/mod/github.com/samber/[email protected]/errors.go:53 +0x1df
github.com/samber/lo.Must0(...)
	/home/whatever/go/pkg/mod/github.com/samber/[email protected]/errors.go:72
github.com/go-kod/kod/cmd/kod/internal.init.func4(0xb8a660, {0xc000032420?, 0x4?, 0x81a17c?})
	/home/whatever/Documents/dev/perso/kod/cmd/kod/internal/struct2interface.go:379 +0xa9
github.com/spf13/cobra.(*Command).execute(0xb8a660, {0xc0000323e0, 0x1, 0x1})
	/home/whatever/go/pkg/mod/github.com/spf13/[email protected]/command.go:987 +0xab1
github.com/spf13/cobra.(*Command).ExecuteC(0xb8a380)
	/home/whatever/go/pkg/mod/github.com/spf13/[email protected]/command.go:1115 +0x3ff
github.com/spf13/cobra.(*Command).Execute(...)
	/home/whatever/go/pkg/mod/github.com/spf13/[email protected]/command.go:1039
github.com/go-kod/kod/cmd/kod/internal.Execute()
	/home/whatever/Documents/dev/perso/kod/cmd/kod/internal/root.go:33 +0x1a
main.main()
	/home/whatever/Documents/dev/perso/kod/cmd/kod/main.go:6 +0xf
exit status 2
task: Failed t

mockgen is installed, but code expects the github.com/labstack/echo/v4 to be imported

so it should depend on task mod

task `generate:struct2interface` is broken because of `examples/infra/redis/kod_gen_interface.go`

$ task generate:struct2interface
task: Task "install:mockgen" is up to date
task: [generate:struct2interface] go run ./cmd/kod struct2interface .
2024/06/08 08:30:42 Loading input failed: examples/infra/redis/kod_gen_interface.go:11:8: failed parsing returns: examples/infra/redis/kod_gen_interface.go:11:12: unknown package "redis"
panic: mockgen error: exit status 1

goroutine 1 [running]:
github.com/samber/lo.must({0x7b1d80, 0xc000417610}, {0x0, 0x0, 0x0})
	/home/whatever/go/pkg/mod/github.com/samber/[email protected]/errors.go:53 +0x1df
github.com/samber/lo.Must0(...)
	/home/whatever/go/pkg/mod/github.com/samber/[email protected]/errors.go:72
github.com/go-kod/kod/cmd/kod/internal.init.func4(0xb8a660, {0xc000032420?, 0x4?, 0x81a17c?})
	/home/whatever/Documents/dev/perso/kod/cmd/kod/internal/struct2interface.go:379 +0xa9
github.com/spf13/cobra.(*Command).execute(0xb8a660, {0xc0000323e0, 0x1, 0x1})
	/home/whatever/go/pkg/mod/github.com/spf13/[email protected]/command.go:987 +0xab1
github.com/spf13/cobra.(*Command).ExecuteC(0xb8a380)
	/home/whatever/go/pkg/mod/github.com/spf13/[email protected]/command.go:1115 +0x3ff
github.com/spf13/cobra.(*Command).Execute(...)
	/home/whatever/go/pkg/mod/github.com/spf13/[email protected]/command.go:1039
github.com/go-kod/kod/cmd/kod/internal.Execute()
	/home/whatever/Documents/dev/perso/kod/cmd/kod/internal/root.go:33 +0x1a
main.main()
	/home/whatever/Documents/dev/perso/kod/cmd/kod/main.go:6 +0xf
exit status 2
task: Failed to run task "generate:struct2interface": exit status 1

infra/redis package imports "github.com/redis/go-redis/v9"

but Go reports the file as missing, calling task mod doesn't solve issue

how to declare multi implements of one interface?

https://github.com/leopku/kodtest/blob/master/cmd/demo3.go

type IStore interface {
	Get(string) any
	Set(string, any) error
}

type MemoryStore struct {
	kod.Implements[...]  <--
}

func (MemoryStore) Get(string) any {
	fmt.Println("dummy get called")
	return "dummy string"
}

func (MemoryStore) Set(string, any) error {
	fmt.Println("dummy set called")
	return nil
}

type DiskStore struct {
	kod.Implements[...]  <--
}

func (DiskStore) Get(string) any {
	fmt.Println("dummy get called")
	return "dummy string"
}

func (DiskStore) Set(string, any) error {
	fmt.Println("dummy set called")
	return nil
}

tasks `install:mockgen` and `install:golangci-lint` are installing specific version for all my machine

If I have golang 1.59.0, or a custom version locally

the code will nuke it as it would install the version needed by your tool, which is the 1.56.0 for now

same for mockgen

kod/Taskfile.yml

Lines 78 to 92 in 795c83b

install:mockgen:
vars:
VERSION: cat go.mod|grep go.uber.org/mock |awk -F ' ' '{print $2}'
status:
- go version -m $GOPATH/bin/mockgen | grep go.uber.org/mock | grep {{.VERSION}}
cmd: |
go install go.uber.org/mock/mockgen@{{.VERSION}}
install:golangci-lint:
vars:
VERSION: v1.56.2
status:
- go version -m $GOPATH/bin/golangci-lint | grep github.com/golangci/golangci-lint | grep {{.VERSION}}
cmd: |
go install github.com/golangci/golangci-lint/cmd/golangci-lint@{{.VERSION}}

Multi kod.Main implements supported?

My app was a command-line app based on cobra and had many sub commands as different entrances.

  • cmd/demo1.go
type demo1Impl struct {
	kod.Implements[kod.Main]
	db kod.Ref[db.Component]
}

...
  • cmd/demo2.go
type demo2Impl struct {
	kod.Implements[kod.Main]
	db kod.Ref[db.Component]
}

...

result as

Error: components [github.com/go-kod/kod/Main], error vertex already exists

component was not registered

  • environments
macOS 11.7.10
go v1.22.4
kod v0.9.1
  • pkg/infra/db/db.go
package db

import (
	"context"
	"database/sql"

	"github.com/go-kod/kod"
	"github.com/go-kod/kod/ext/client/ksql"
)

type DBComponent struct {
	kod.Implements[Component]
	kod.WithConfig[config]

	db *sql.DB
}

type config struct {
	DbConfig ksql.Config
}

func (ins *DBComponent) Init(ctx context.Context) error {
	ins.db = ins.Config().DbConfig.Build()
	return nil
}
  • demo.go
type demoImpl struct {
	kod.Implements[kod.Main]
	db kod.Ref[db.DBComponent]
}

func main() {
		fmt.Println("demo called")
		kod.Run(context.Background(), func(ctx context.Context, impl *demoImpl) error {
			dbIns := impl.db.Get()
			impl.L(ctx).Info(dbIns.Config().DbConfig.DriverName)
			return nil
		})
}
  • run
kod struct2interface ./...
kod generate ./...
  • result
Error: component implementation struct cmd.demoImpl has field kod.Ref[github.com/leopku/emeraldpanel/pkg/infra/db.DBComponent], but component db.DBComponent was not registered; maybe you forgot to run 'kod generate'

Any advice?

golangci config file is missing

The project should have a .golangci.yml or at least Taskfile.yml should use golangci-lint run --no-config

because the way golangci-lint works when looking for configuration files, if there is no golangci file in current project, it will try the parent folders. If there is one it would apply its settings

I do have this locally to be able to test random Go project with a full set of rules. my settings are way more restrictive than the default values.

Originally posted by @ccoVeille in #149 (comment)

Failed to get a member of a ref instance that it was a member of a ref instance?

See https://github.com/leopku/kodtest/blob/b2389ba401a69b0343de2ca89d1e2c85dcc0f031/cmd/demo2.go#L43

type demo2Impl struct {
	kod.Implements[Demo2]
	kod.LazyInit
	migrate kod.Ref[migrate.IMigrate]
}

...

	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("demo2 called")
		err := kod.Run(context.Background(), func(ctx context.Context, ins *app) error {
			// pp.Println(ins.demo2.Get())
			demo2Ins := ins.demo2.Get()
			demo2Ins.migrate.Get().Hello()  <-- this line
			return nil
		}, kod.WithOpenTelemetryDisabled())
		cobra.CheckErr(err)
	},

demo2Ins.migrate undefined (type Demo2 has no field or method migrate, but does have Migrate)

test panic: I think something is broken

--- FAIL: TestGraph (0.01s)
    --- FAIL: TestGraph/linux_amd64 (0.01s)
        callgraph_test.go:32: 
            	Error Trace:	kod/cmd/kod/internal/callgraph_test.go:32
            	Error:      	Expected nil, but got: &exec.ExitError{ProcessState:(*os.ProcessState)(0xc000012360), Stderr:[]uint8(nil)}
            	Test:       	TestGraph/linux_amd64
panic: open graphcase: no such file or directory [recovered]
	panic: open graphcase: no such file or directory

Where Component comes from? undefined: Component

package account

import (
	"context"

	"github.com/leopku/emeraldpanel/pkg/models/auth"

	"github.com/go-kod/kod"
	"github.com/go-kod/kod/ext/client/ksql"
)

type AccountService struct {
	kod.Implements[Component]
	kod.WithConfig[config]

	query *auth.Queries
}

type config struct {
	DbConfig ksql.Config
}

func (svc *AccountService) Init(ctx context.Context) error {
	db := svc.Config().DbConfig.Build()
	svc.query = auth.New(db)
	return nil
}

func (svc *AccountService) Query() *auth.Queries {
	return svc.query
}

result as: undefined: Component

All sample codes wrote as kod.Implements[Component], where Component comes from?

`installer:mockgen` task is not valid

Trying to launch go generate ./... showed me this also

cmd/kod/internal/watcher_test.go:12: running "mockgen": exec: "mockgen": executable file not found in $PATH

So I tried to use

$ task install:mockgen
task: Task "install:mockgen" is up to date

which is wrong as I don't have mockgen installed

      - go version -m $GOPATH/bin/mockgen | grep go.uber.org/mock | grep {{.VERSION}}

This code behaves wrongly when mockgen is not installed.

Also, this piece of code relies on GOPATH env variable, that is not always defined (my case). go env GOPATH should be preferred whenever needed.

Also, the installer checks for GOPATH/bin, while code will simply call mockgen directly, without a path so it should check for the presence of mockgen in PATH first

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.