README ¶
Kit
Kit is a set of tools that can be used to enhance your service.
Installation
To start using latest version of Kit, just run: To install kit, follow these steps:
- Open a terminal window.
- Navigate to the project directory where you want to install the library
- Run the following command:
go get github.com/lucianogarciaz/kit
Usage
Here are a few examples of how to use Kit:
Observability
Provides a simple interface for logging. It allows the user to define a custom logger with options for log level, message, and payload.
Logger
The obs package provides a basic implementation of a logger, but you can also create your own custom logger by implementing the Logger interface.
Explain more
type Logger interface {
Log(level LogLevel, message string, payload ...PayloadEntry) error
}
The Log() method takes a log level, a message string, and an optional list of payload entries. You can define your own implementation of the Log() method to customize how log messages are processed and formatted.
Creating a Basic Logger
To create a basic logger with default options, you can use the NewBasicLogger() function:
logger := obs.NewBasicLogger()
The default logger writes log messages to os.Stdout using the json format.
Logging Messages
To log a message, you can use the Log() method of the logger. The method takes a log level, a message string, and an optional list of payload entries. The log level can be one of the predefined constants LevelDebug, LevelInfo, LevelWarn, or LevelError. For example:
logger.Log(obs.LevelInfo, "Hello, world!")
Customizing the Logger
You can customize the behavior of the logger by passing one or more options to the NewBasicLogger() function. The available options are:
- MarshalerOpt: sets the Marshaler used to encode log messages. The default is jsonMarshaler.
- WriterOpt: sets the writer to which log messages are written. The default is os.Stdout. For example, to create a logger that writes log messages to a file instead of os.Stdout, you can use the following code:
file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
log.Fatal(err)
}
logger := obs.NewBasicLogger(
obs.WriterOpt(file),
)
logger.Log(obs.LevelInfo, "some log")
Advanced Usage
The obs package provides a basic implementation of a logger, but you can also create your own custom logger by implementing the Logger interface.
CQS
Summary
Command-Query Separation (CQS) pattern is for handling command and queries in a software system. In CQS, commands and queries are separated into two distinct types of operations, each with its own interface and handler. While commands change the state of the system, queries retrieve data from the system without modifying it.This pattern provides several benefits, including better code organization, easier testing, and improved scalability. By separating queries from commands, developers can focus on each type of operation separately and optimize their implementations for their specific use cases.
The cqs package provides a flexible way to handle queries by defining interfaces for queries, query handlers, and query result types.
Additionally, it provides a middleware function that allows developers to add additional functionality to the query/command handling pipeline, such as caching or logging, without modifying the underlying query handler.
Queries
explain more:
var _ Query = &HelloQuery{}
// Define the Query type.
type HelloQuery struct {
Id string
}
func (h HelloQuery) QueryName() string {
return "hello_query"
}
var _ QueryHandler[HelloQuery, QueryResult] = &HelloQueryHandler{}
// Define the QueryHandler type.
type HelloQueryHandler struct {
someRepo SomeRepository
}
// Implement the Handle method for the QueryHandler type.
func (h HelloQueryHandler) Handle(ctx context.Context, query HelloQuery) (QueryResult, error) {
hello, err := h.someRepo.GetById(ctx, query.Id)
if err != nil {
return nil, err
}
return hello, nil
}
// implementation of a logger middlware
func LoggerMiddleware[Q Query, R QueryResult](log Logger) QueryHandlerMiddleware[Q, R] {
return func(h QueryHandler[Q, R]) QueryHandler[Q, R] {
return queryHandlerFunc[Q, R](func(ctx context.Context, query Q) (R, error) {
log.Info("you will see this message before the handle is called")
result, err := h.Handle(ctx, query)
log.Info("you will see this message after the handle is called")
if err != nil {
log.Error(fmt.Errorf("something went wrong, %w", err))
return result, err
}
log.Info(fmt.Sprintf("query: %s was executed correctly", query.QueryName()))
return result, err
})
}
}
type Logger interface {
Info(string)
Error(error)
}
func qhMw[Q Query, R QueryResult](logger Logger) QueryHandlerMiddleware[Q, R] {
return QueryHandlerMultiMiddleware(
// Be careful ⚠️ the order of the mid. are important
LoggerMiddleware[Q, R](logger),
)
}
func main() {
handler := HelloQueryHandler{}
qh := qhMw[HelloQuery, QueryResult](JSONLogger{})(handler)
result, err := qh.Handle(context.Background(), HelloQuery{Id: "some-id"})
if err != nil {
// do something
return
}
// do something else
_ = result
}
Command Handlers
explain more:
var _ Command = &HelloCommand{}
// Define the Command type.
type HelloCommand struct {
Id vo.ID
Name string
}
func (h HelloCommand) CommandName() string {
return "hello_command"
}
var _ CommandHandler[HelloCommand] = &HelloCommandHandler{}
type SomeRepository interface {
Save(ctx context.Context, id vo.ID, name string) error
}
// Define the CommandHandler type.
type HelloCommandHandler struct {
someRepo SomeRepository
}
// Implement the Handle method for the CommandHandler type.
func (h HelloCommandHandler) Handle(ctx context.Context, cmd HelloCommand) ([]Event, error) {
err := h.someRepo.Save(ctx, cmd.Id, cmd.Name)
if err != nil {
return nil, err
}
return nil, nil
}
// implementation of a logger middlware
func LoggerMiddleware[C Command](log Logger) CommandHandlerMiddleware[C] {
return func(h CommandHandler[C]) CommandHandler[C] {
return CommandHandlerFunc[C](func(ctx context.Context, cmd C) ([]Event, error) {
log.Info("you will see this message before the handle is called")
events, err := h.Handle(ctx, cmd)
log.Info("you will see this message after the handle is called")
if err != nil {
log.Error(fmt.Errorf("something went wrong, %w", err))
return events, err
}
log.Info(fmt.Sprintf("command: %s was executed correctly", cmd.CommandName()))
return events, err
})
}
}
type Logger interface {
Info(string)
Error(error)
}
func chMw[C Command](logger Logger) CommandHandlerMiddleware[C] {
return CommandHandlerMultiMiddleware(
// Be careful ⚠️ the order of the mid. are important
OtherMiddlware[C](logger),
LoggerMiddleware[C](logger),
)
}
var _ Logger = &JSONLogger{}
type JSONLogger struct{}
func (J JSONLogger) Info(s string) {}
func (J JSONLogger) Error(err error) {}
func main() {
handler := HelloCommandHandler{}
ch := chMw[HelloCommand](JSONLogger{})(handler)
events, err := ch.Handle(context.Background(), HelloCommand{Id: vo.NewID(), Name: "some-name"})
if err != nil {
// do something
return
}
// do something else
_ = events
}
Value objects
Id
usage examples
import (
"github.com/lucianogarciaz/kit
)
func main() {
id := kit.NewID()
id.String()
}
Pointers
usage examples
IntPtr
func someOtherFunc(a *int) {
// does something
}
func main() {
someOtherFunc(IntPtr(123))
}
IntValue
func pointerFunc() *int {
var in = 123
return &in
}
func someOtherFunc(a int) {
// does something
}
func main() {
someOtherFunc(IntValue(pointerFunc()))
}
Int32Ptr
func someOtherFunc(a *int32) {
// does something
}
func main() {
someOtherFunc(Int32Ptr(3123))
}
Int32Value
func pointerFunc() *int32 {
var in int32 = 123
return &in
}
func someOtherFunc(a int32) {
// does something
}
func main() {
someOtherFunc(Int32Value(pointerFunc()))
}
Int64Ptr
func someOtherFunc(a *int64) {
// does something
}
func main() {
someOtherFunc(Int64Ptr(3123))
}
Int64Value
func pointerFunc() *int64 {
var in int64 = 123
return &in
}
func someOtherFunc(a int64) {
// does something
}
func main() {
someOtherFunc(Int64Value(pointerFunc()))
}
Float32Ptr
func someOtherFunc(a *float32) {
// does something
}
func main() {
someOtherFunc(Float32Ptr(123.2))
}
Float32Value
func pointerFunc() *float32 {
var in float32 = 123
return &in
}
func someOtherFunc(a float32) {
// does something
}
func main() {
someOtherFunc(Float32Value(pointerFunc()))
}
Float64Ptr
func someOtherFunc(a *float64) {
// does something
}
func main() {
someOtherFunc(Float64Ptr(123.2))
}
Float64Value
func pointerFunc() *float64 {
var in float64 = 123
return &in
}
func someOtherFunc(a float64) {
// does something
}
func main() {
someOtherFunc(Float64Value(pointerFunc()))
}
BoolValue
func pointerFunc() *bool {
var in = true
return &in
}
func someOtherFunc(a bool) {
// does something
}
func main() {
someOtherFunc(BoolValue(pointerFunc()))
}
BoolPtr
func someOtherFunc(a *bool) {
// does something
}
func main() {
someOtherFunc(BoolPtr(true))
}
StringPtr
func someOtherFunc(a *string) {
// does something
}
func main() {
someOtherFunc(StringPtr("some-string"))
}
StringValue
func pointerFunc() *string {
var in = "something"
return &in
}
func someOtherFunc(a string) {
// does something
}
func main() {
someOtherFunc(StringValue(pointerFunc()))
}
TimePtr
func someOtherFunc(a *time.Time) {
// does something
}
func main() {
someOtherFunc(TimePtr(time.Now()))
}
TimeValue
func pointerFunc() *time.Time {
var in = time.Now()
return &in
}
func someOtherFunc(a time.Time) {
// does something
}
func main() {
someOtherFunc(TimeValue(pointerFunc()))
}
DateTime
usage examples
func main() {
dt := vo.DateTimeNow()
dt.Format(time.RFC3339Nano)
dt2 := vo.DateTimeNow()
dt.Equal(dt2) //false
dt.IsZero() //false
err := dt.Scan(time.Now()) // err = false
// implements marshalJSON
bt, err := dt.MarshalJSON() //err = false
var emptyDt vo.DateTime
err = emptyDt.UnmarshalJSON(bt) //err = false
emptyDt.Equal(dt) // true
}
Contributing
I welcome contributions from the community! To contribute to Kit, follow these steps:
- Fork the repository.
- Create an issue where we can debate about what you want to add
- Create a new branch for your changes: git checkout -b my-feature-branch
- Please document the usage in the README.md file
- Make your changes and commit them
- Push your changes to your fork: git push origin my-feature-branch
- Submit a pull request.
Acknowledgements
I would like to thank also the people who have inspired me:
- @gonzaloserrano: Gonzalo Serrano
- @xabi93: Xavi Martinez
- @jmaeso: Joan Maeso
- @mountolive: Leandro Guercio
- @jkmrto: Juan Carlos Martinez
- @maitesin: Oscar Forner Martinez
- @theskyinflames: Jaume Araús
- Typeformers and specially subscriptions team 🧡