kod

package module
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 10, 2024 License: Apache-2.0 Imports: 24 Imported by: 29

README

Kod

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

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) Stop(context.Context) error {
    // ...
}
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

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 to configure 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 a 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"`
}
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.

Acknowledge

This project was heavily inspired by ServiceWeaver.

Star History

Star History Chart

Documentation

Index

Constants

View Source
const (
	PkgPath = "github.com/go-kod/kod"
)

Variables

View Source
var NewLogObserver = kslog.NewLogObserver
View Source
var Register = registry.Register

Register registers the given component implementations.

Functions

func Fake

func Fake[T any](impl any) fakeComponent

func Run

func Run[T any, _ PointerToMain[T]](ctx context.Context, run func(context.Context, *T) error, opts ...func(*options)) error

Run initializes and runs the application with the provided main component and options.

func RunTest

func RunTest[T any](t testing.TB, body func(context.Context, T), opts ...func(*options))

func WithConfigFile

func WithConfigFile(filename string) func(*options)

WithConfigFile is an option setter for specifying a configuration file.

func WithFakes

func WithFakes(fakes ...fakeComponent) func(*options)

WithFakes is an option setter for specifying fake components for testing.

func WithInterceptors added in v0.6.0

func WithInterceptors(interceptors ...interceptor.Interceptor) func(*options)

WithInterceptors is an option setter for specifying interceptors.

func WithLogWrapper

func WithLogWrapper(h func(slog.Handler) slog.Handler) func(*options)

WithLogWrapper is an option setter for specifying a logger.

func WithRegistrations

func WithRegistrations(regs ...*Registration) func(*options)

WithRegistrations is an option setter for specifying component registrations.

Types

type Implements

type Implements[T any] struct {
	// contains filtered or unexported fields
}

Implements[T any] provides a common structure for components, with logging capabilities and a reference to the component's interface.

func (*Implements[T]) L

func (i *Implements[T]) L(ctx context.Context) *slog.Logger

L returns the associated logger.

type InstanceOf

type InstanceOf[T any] interface {
	// contains filtered or unexported methods
}

InstanceOf[T any] is an interface for asserting implementation of an interface T.

type Kod

type Kod struct {
	// contains filtered or unexported fields
}

Kod represents the core structure of the application, holding configuration and component registrations.

func FromContext

func FromContext(ctx context.Context) *Kod

FromContext returns the Kod value stored in ctx, if any.

func (*Kod) Config

func (k *Kod) Config() kodConfig

Config returns the current configuration of the Kod instance.

func (*Kod) L

func (k *Kod) L(ctx context.Context) *slog.Logger

L() returns the logger of the Kod instance.

func (*Kod) LevelVar

func (k *Kod) LevelVar() *slog.LevelVar

LevelVar returns the log level variable of the Kod instance.

type LocalStubFnInfo

type LocalStubFnInfo = registry.LocalStubFnInfo

LocalStubFnInfo is the information passed to LocalStubFn.

type Main

type Main interface{}

Main is the interface that should be implemented by an application's main component. The main component is the entry point of the application, and is expected to be a struct that embeds Implements[Main].

Example:

type app struct {
	kod.Implements[kod.Main]
}

func main() {
	kod.Run(context.Background(), func(ctx context.Context, main *app) error {
		fmt.Println("Hello, World!")
		return nil
	})
}

type ObservedLogs

type ObservedLogs kslog.ObservedLogs

type PointerToMain

type PointerToMain[T any] interface {
	*T
	InstanceOf[Main]
}

PointerToMain is a type constraint that asserts *T is an instance of Main (i.e. T is a struct that embeds weaver.Implements[weaver.Main]).

type Ref

type Ref[T any] struct {
	// contains filtered or unexported fields
}

Ref[T any] is a reference holder to a value of type T. The reference is expected to be a field of a component struct. The value is set by the framework, and is accessible via the Get() method.

Example:

type app struct {
	kod.Implements[kod.Main]
	component kod.Ref[example.Component]
}

func main() {
	kod.Run(context.Background(), func(ctx context.Context, main *app) error {
		component := main.component.Get()
		// ...
	})
}

func (Ref[T]) Get

func (r Ref[T]) Get() T

Get returns the held reference value.

type Registration

type Registration = registry.Registration

Registration is the registration information for a component.

type WithConfig

type WithConfig[T any] struct {
	// contains filtered or unexported fields
}

WithConfig[T any] is a struct to hold configuration of type T. The struct is expected to be a field of a component struct. The configuration is loaded from a file, and is accessible via the Config() method.

Example:

type app struct {
	kod.Implements[kod.Main]
	kod.WithConfig[appConfig]
}

type appConfig struct {
	Host string
	Port int
}

func main() {
	kod.Run(context.Background(), func(ctx context.Context, main *app) error {
		fmt.Println("config:", main.Config())
	})
}

func (*WithConfig[T]) Config

func (wc *WithConfig[T]) Config() *T

Config returns a pointer to the config.

Directories

Path Synopsis
cmd
kod
kod/internal
Package files contains file-related utilities.
Package files contains file-related utilities.
examples module
ext module
internal
tests module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL