jetti

module
v2.7.0 Latest Latest
Warning

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

Go to latest
Published: Jul 24, 2023 License: MIT

README

jetti

제티(jetti)는 고 언어 프로젝트에 어느정도 정규화된 프로젝트 구성을 적용하기 위한 도구입니다.

지향점

[WIP]

install

다음 명령어를 실행하면 $HOME/go/binjetti 바이너리가 설치됩니다.

latest로 버전을 특정하면 항상 최신 버전을 설치합니다.

go install github.com/snowmerak/jetti/v2/cmd/jetti@latest

new

new는 새로운 프로젝트나 커맨드 패키지를 생성합니다.

new module

jetti new <module-name>을 실행하면 현재 폴더에서 go mod <module-name>을 실행하면서 다음과 같은 기본 폴더들을 만들어줍니다.

➜  tree .
.
├── README.md
├── cmd
│   └── doc.go
├── go.mod
├── internal
│   └── doc.go
└── lib
    └── doc.go

4 directories, 5 files
new command

jetti new --cmd <cmd-name>을 실행하면 현재 폴더 내의 cmd 폴더에 <cmd-name> 폴더를 만들고, main.go 파일을 만들어줍니다.

다음 예시는 jetti new --cmd prac를 실행한 결과입니다.

➜  jetti new --cmd prac
➜  tree .
.
├── README.md
├── cmd
│   ├── doc.go
│   └── prac
│       └── main.go
├── go.mod
├── internal
│   └── doc.go
└── lib
    └── doc.go

5 directories, 6 files
new proto

jetti new --proto <path/name>을 실행하면 현재 폴더 내의 <path> 폴더를 만들고, <name>.proto 파일을 만듭니다.

다음 예시는 jetti new --proto model/proto/person를 실행한 결과입니다.

syntax = "proto3";

package person;

option go_package = "model/proto/person";

run

runcmd 내의 커맨드 패키지를 실행하는 역할을 합니다.

jetti run <cmd-name>을 실행하면 cmd/<cmd-name> 폴더 내의 고 파일들을 실행합니다.

추가로 jetti run <cmd-name> <args>...을 실행하여 커맨드 패키지에 인자를 전달할 수 있습니다.
사실상 go run과 동일합니다.

request scope data

request scope datacontext.ContextWithValue를 편리하게 이용할 수 있게 해주는 기능입니다.

request scope data의 핵심은 동등, 혹은 하위 문맥에서 동일한 데이터를 공유하는 것입니다.

예시

./lib/config 폴더를 만들고 config.go 파일을 만들어 다음과 같이 작성합니다.

package config

// jetti:request redis postgres
type Config struct {
}

jetti:request 주석을 통해 redispostgres 빈을 등록했습니다.

이제 터미널에 jetti generate를 입력하면 ./lib/redis.context.go./lib/postgres.context.go 파일이 생성됩니다.

// postgres.context.go
package config

import "context"

type PostgresContextKey string

func PushPostgres(ctx context.Context, v *Config) context.Context {
	return context.WithValue(ctx, PostgresContextKey("Postgres"), v)
}

func GetPostgres(ctx context.Context) (*Config, bool) {
	v, ok := ctx.Value(PostgresContextKey("Postgres")).(*Config)
	return v, ok
}
package config

import "context"

type RedisContextKey string

func PushRedis(ctx context.Context, v *Config) context.Context {
	return context.WithValue(ctx, RedisContextKey("Redis"), v)
}

func GetRedis(ctx context.Context) (*Config, bool) {
	v, ok := ctx.Value(RedisContextKey("Redis")).(*Config)
	return v, ok
}

이제 단일 컨텍스트를 생성한 후, Push 메서드를 통해 데이터를 컨텍스트에 추가하고, Get 메서드를 통해 데이터를 가져올 수 있습니다.

optional parameter

optional parameterjetti:parameter 주석을 통해 생성할 수 있습니다.

옵셔널 패러미터는 기존의 프리미티브 타입, 혹은 구조체에 기본값과 값 변경을 위한 함수를 받아 기본값을 변형하여 새로운 패러미터를 반환합니다.

예시

./lib/person 폴더를 만들고 person.go 파일을 만들어 다음과 같이 작성합니다.

package person

// jetti:parameter
type Person struct {
	Name string
	Age  int
}

jetti generate를 실행하면 ./lib/person.parameter.go 파일이 생성됩니다.

package person

type PersonOptional func(*Person) *Person

func ApplyPerson(defaultValue Person, fn ...PersonOptional) *Person {
	param := &defaultValue
	for _, f := range fn {
		param = f(param)
	}
	return param
}

ApplyPerson 함수에 기본값과 변형 함수를 전달하여 새로운 Person 구조체를 생성합니다.

json/yaml to go

go-jsonstruct 라이브러리를 이용해서 json/yaml 파일을 go 구조체로 변환할 수 있습니다.

파싱에는 각각 goccy/go-jsongoccy/go-yaml 라이브러리를 사용합니다.

예시

./config/json_prac.json./config/yaml_prac.yaml 파일을 만들고 다음과 같이 작성합니다.

{
  "name": "snowmerak",
  "version": "1.3.2",
  "author": "snowmerak",
  "dependencies": {
    "go": "github.com/golang/go",
    "rust": "github.com/rust-lang/rust"
  }
}
name: snowmerak
version: 1.3.2
author: snowmerak
dependencies:
  go: github.com/golang/go
  rust: github.com/rust-lang/rust

그리고 jetti generate를 실행하면 다음 파일 들이 생성됩니다.

// json_prac.json.go
package config

import "github.com/goccy/go-json"
import "io"
import "os"

func JsonPracFromJSON(data []byte) (*JsonPrac, error) {
	v := new(JsonPrac)
	if err := json.Unmarshal(data, v); err != nil {
		return nil, err
	}
	return v, nil
}

func JsonPracFromFile(path string) (*JsonPrac, error) {
	f, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}
	return JsonPracFromJSON(f)
}

func (jsonprac *JsonPrac) Marshal2JSON() ([]byte, error) {
	return json.Marshal(jsonprac)
}

func (jsonprac *JsonPrac) Encode2JSON(w io.Writer) error {
	return json.NewEncoder(w).Encode(jsonprac)
}

type JsonPrac struct {
	Author       string `json:"author"`
	Dependencies struct {
		Go   string `json:"go"`
		Rust string `json:"rust"`
	} `json:"dependencies"`
	Name    string `json:"name"`
	Version string `json:"version"`
}
// yaml_prac.yaml.go
package config

import "github.com/goccy/go-yaml"
import "io"
import "os"

func YamlPracFromYAML(data []byte) (*YamlPrac, error) {
	v := new(YamlPrac)
	if err := yaml.Unmarshal(data, v); err != nil {
		return nil, err
	}
	return v, nil
}

func YamlPracFromFile(path string) (*YamlPrac, error) {
	f, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}
	return YamlPracFromYAML(f)
}

func (yamlprac *YamlPrac) Marshal2YAML() ([]byte, error) {
	return yaml.Marshal(yamlprac)
}

func (yamlprac *YamlPrac) Encode2YAML(w io.Writer) error {
	return yaml.NewEncoder(w).Encode(yamlprac)
}

type YamlPrac struct {
	Author       string `yaml:"author"`
	Dependencies struct {
		Go   string `yaml:"go"`
		Rust string `yaml:"rust"`
	} `yaml:"dependencies"`
	Name    string `yaml:"name"`
	Version string `yaml:"version"`
}

protobuf/flatbuffers generating

제티는 프로젝트 내부의 프로토버퍼와 플랫버퍼 파일을 찾으면 자동으로 고 코드로 컴파일 하는 커맨드를 실행합니다.

이를 위해 protocflatc가 필요합니다.

protoc/grpc 설치

여기를 참고해 protoc 및 고 코드 생성을 위한 플러그인을 설치합니다.

flatc 설치

여기를 참고해 flatc를 설치합니다.

굳이 빌드 하지 않더라도 사용하는 환경의 패키지 매니저를 통해 설치할 수 있습니다.

사용법

./proto 디렉토리를 만들고 ./proto/test/test.proto 파일을 만듭니다.

syntax = "proto3";

package test;

option go_package = "./test";

message Test {
  string name = 1;
  int32 age = 2;
}

그리고 jetti generate를 실행하면 ./gen/grpc/proto/test/test.pb.go에 파일을 생성합니다.

object pool

제티는 sync.Poolchan T을 사용한 두가지 풀을 만들 수 있습니다.

sync pool

jetti:pool을 주석에 작성함으로 풀을 생성할 수 있습니다.

두 가지 풀 중, sync.Pool은 jetti:pool sync:<alias>로 생성할 수 있습니다.

<alias>는 풀의 이름을 지정합니다.

// jetti:pool sync:people
type Person struct {
    Name string
    Age  int
}

위와 같이 주석을 작성하면 sync.Pool을 이용한 people 풀이 생성됩니다.

package person

import "sync"
import "errors"
import "runtime"

var errPeopleCannotGet error = errors.New("cannot get people")

type PeoplePool struct {
	pool *sync.Pool
}

func (p *PeoplePool) Get() (*Person, error) {
	v := p.pool.Get()
	if v == nil {
		return nil, errPeopleCannotGet
	}
	return v.(*Person), nil
}

func (p *PeoplePool) GetWithFinalizer() (*Person, error) {
	v := p.pool.Get()
	if v == nil {
		return nil, errPeopleCannotGet
	}
	runtime.SetFinalizer(v, func(v interface{}) {
		p.pool.Put(v)
	})
	return v.(*Person), nil
}

func (p *PeoplePool) Put(v *Person) {
	p.pool.Put(v)
}

func NewPeoplePool() PeoplePool {
	return PeoplePool{
		pool: &sync.Pool{
			New: func() interface{} {
				return new(Person)
			},
		},
	}
}

func IsPeopleCannotGetErr(err error) bool {
	return errors.Is(err, errPeopleCannotGet)
}
chan pool

채널을 사용한 풀은 jetti:pool chan:<alias>로 생성할 수 있습니다.

이 풀의 경우엔, 최대 풀링 가능한 오브젝트 수를 제한할 때 유용하게 사용할 수 있습니다.

// jetti:pool sync:people chan:candidate
type Person struct {
	Name string
	Age  int
}

방금 예제에서 sync:people 뒤에 chan:candidate를 추가해서 생성하면, 추가적으로 다음 파일도 생성됩니다.

package person

import (
	"runtime"
	"time"
)

type CandidatePool struct {
	pool    chan *Person
	timeout time.Duration
}

func (c *CandidatePool) Get() *Person {
	after := time.After(c.timeout)
	select {
	case v := <-c.pool:
		return v
	case <-after:
		return new(Person)
	}
}

func (c *CandidatePool) GetWithFinalizer() *Person {
	after := time.After(c.timeout)
	resp := (*Person)(nil)
	select {
	case v := <-c.pool:
		resp = v
	case <-after:
		resp = new(Person)
	}
	runtime.SetFinalizer(resp, func(v interface{}) {
		c.pool <- v.(*Person)
	})
	return resp
}

func (c *CandidatePool) Put(v *Person) {
	select {
	case c.pool <- v:
	default:
	}
}

func NewCandidatePool(size int, timeout time.Duration) CandidatePool {
	pool := make(chan *Person, size)
	return CandidatePool{
		pool:    pool,
		timeout: timeout,
	}
}

sync pool과 다른 점으로 전체 채널 길이와 채널에서 값을 가져올 시간의 제한을 지정합니다.

optional

jetti:optional을 사용하여 해당 타입의 옵셔널 타입을 만들 수 있습니다.

package person

// jetti:optional
type Person struct {
	Name string
	Age  int
}

// jetti:optional
type People [100]Person

예시에서는 person 패키지와 이름이 같은 PersonPerson 타입의 배열인 People을 옵셔널 타입으로 만들었습니다.

package person

type OptionalPeople struct {
	value *People
	valid bool
}

func (o *OptionalPeople) Unwrap() *People {
	if !o.valid {
		panic("unwrap a none value")
	}
	return o.value
}

func (o *OptionalPeople) IsSome() bool {
	return o.valid
}

func (o *OptionalPeople) IsNone() bool {
	return !o.valid
}

func (o *OptionalPeople) UnwrapOr(defaultValue *People) *People {
	if !o.valid {
		return defaultValue
	}
	return o.value
}

func SomePeople(value *People) OptionalPeople {
	return OptionalPeople{
		value: value,
		valid: true,
	}
}

func NonePeople() OptionalPeople {
	return OptionalPeople{
		valid: false,
	}
}

먼저 People은 위와같이 OptionalPeople 타입으로 감싸집니다.

그리고 SomePeopleNonePeople 함수를 통해 생성할 수 있으며, UnwrapUnwrapOr 함수를 통해 값을 꺼낼 수 있습니다.

주의할 점은 Unwrap은 패닉을 발생할 수 있기에 IsSome을 통해 값이 있는지 확인하고 사용해야 합니다.

package person

type OptionalPerson struct {
	value *Person
	valid bool
}

func (o *OptionalPerson) Unwrap() *Person {
	if !o.valid {
		panic("unwrap a none value")
	}
	return o.value
}

func (o *OptionalPerson) IsSome() bool {
	return o.valid
}

func (o *OptionalPerson) IsNone() bool {
	return !o.valid
}

func (o *OptionalPerson) UnwrapOr(defaultValue *Person) *Person {
	if !o.valid {
		return defaultValue
	}
	return o.value
}

func Some(value *Person) OptionalPerson {
	return OptionalPerson{
		value: value,
		valid: true,
	}
}

func None() OptionalPerson {
	return OptionalPerson{
		valid: false,
	}
}

Person 타입의 경우엔 OptionalPerson 타입으로 감싸지만, 패키지와 이름이 같기 떄문에 SomeNone 함수를 통해 생성할 수 있습니다.

show

imports

프로젝트 루트 폴더에서 jetti show --imports을 실행함으로 프로젝트 내부에서 각 패키지들이 의존하는 관계를 그래프로 그려줍니다.

그려진 그래프는 루트 폴더 내의 imports.svg 파일로 저장됩니다.

Directories

Path Synopsis
cmd
jetti command
prac command
internal
lib

Jump to

Keyboard shortcuts

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