diff --git a/README.md b/README.md index 3f6aeb8..e4c1c92 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,6 @@ # tinyurl -
- 🔱 CI Actions Status - [![Test Backend](https://github.com/naohito-T/tinyurl/actions/workflows/test_backend.yml/badge.svg?branch=main)](https://github.com/naohito-T/tinyurl/actions/workflows/test_backend.yml) -
+[![Test Backend](https://github.com/naohito-T/tinyurl/actions/workflows/test_backend.yml/badge.svg?branch=main)](https://github.com/naohito-T/tinyurl/actions/workflows/test_backend.yml) ## Overview diff --git a/backend/Makefile b/backend/Makefile index 01ebbda..cfb21ab 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -38,6 +38,9 @@ lint-f: build: GOARCH=arm64 GOOS=linux go build -o bin/main cmd/api/main.go +build-openapi: + go run ./cmd/api/main.go openapi >./api/openapi.yaml + # -------------------------- # Utils # -------------------------- @@ -48,4 +51,4 @@ dynamo.admin: export PORT="4005" && \ npx dynamodb-admin -.PHONY: test sec errcheck staticcheck format lint-all lint build test dynamo.admin +.PHONY: dev test sec errcheck staticcheck format lint-all lint build build-openapi dynamo.admin diff --git a/backend/api/openapi.yaml b/backend/api/openapi.yaml new file mode 100644 index 0000000..b3a91ef --- /dev/null +++ b/backend/api/openapi.yaml @@ -0,0 +1,160 @@ +components: + schemas: + ErrorDetail: + additionalProperties: false + properties: + location: + description: Where the error occurred, e.g. 'body.items[3].tags' or 'path.thing-id' + type: string + message: + description: Error message text + type: string + value: + description: The value at the given location + type: object + ErrorModel: + additionalProperties: false + properties: + $schema: + description: A URL to the JSON Schema for this object. + examples: + - http://localhost:6500/api/v1/schemas/ErrorModel.json + format: uri + readOnly: true + type: string + detail: + description: A human-readable explanation specific to this occurrence of the problem. + examples: + - Property foo is required but is missing. + type: string + errors: + description: Optional list of individual error details + items: + $ref: "#/components/schemas/ErrorDetail" + type: array + instance: + description: A URI reference that identifies the specific occurrence of the problem. + examples: + - https://example.com/error-log/abc123 + format: uri + type: string + status: + description: HTTP status code + examples: + - 400 + format: int64 + type: integer + title: + description: A short, human-readable summary of the problem type. This value should not change between occurrences of the error. + examples: + - Bad Request + type: string + type: + default: about:blank + description: A URI reference to human-readable documentation for the error. + examples: + - https://example.com/errors/example + format: uri + type: string + type: object + GreetingOutput3Body: + additionalProperties: false + properties: + $schema: + description: A URL to the JSON Schema for this object. + examples: + - http://localhost:6500/api/v1/schemas/GreetingOutput3Body.json + format: uri + readOnly: true + type: string + message: + description: Greeting message + examples: + - Hello, world! + type: string + required: + - message + type: object + HealthCheckParams2Body: + additionalProperties: false + properties: + $schema: + description: A URL to the JSON Schema for this object. + examples: + - http://localhost:6500/api/v1/schemas/HealthCheckParams2Body.json + format: uri + readOnly: true + type: string + message: + description: Greeting message + examples: + - Hello, world! + type: string + required: + - message + type: object + securitySchemes: + bearer: + bearerFormat: JWT + scheme: bearer + type: http +info: + title: TinyURL + version: 1.0.0 +openapi: 3.1.0 +paths: + /greeting/{name}: + get: + description: Get a greeting for a person by name. + operationId: get-greeting + parameters: + - description: Name to greet + example: world + in: path + name: name + required: true + schema: + description: Name to greet + examples: + - world + maxLength: 30 + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/GreetingOutput3Body" + description: OK + default: + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ErrorModel" + description: Error + summary: Get a greeting + tags: + - Greetings + /health: + get: + description: Check the health of the service. + operationId: health + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/HealthCheckParams2Body" + description: OK + default: + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ErrorModel" + description: Error + summary: Health Check + tags: + - Greetings +servers: + - url: http://localhost:6500/api/v1 + diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go index dfc6dde..c77c8ac 100644 --- a/backend/cmd/api/main.go +++ b/backend/cmd/api/main.go @@ -25,6 +25,25 @@ type Options struct { Port int `doc:"Port to listen on." short:"p" default:"8888"` } +// /api/v1/openapi.yaml +// initHuma: humaのconfigを初期化 +func initHuma() huma.Config { + config := huma.DefaultConfig(configs.OpenAPITitle, configs.OpenAPIVersion) + config.Servers = []*huma.Server{ + {URL: configs.OpenAPIDocServerPath}, + } + + config.Components.SecuritySchemes = map[string]*huma.SecurityScheme{ + "bearer": { + Type: "http", + Scheme: "bearer", + BearerFormat: "JWT", + }, + } + config.DocsPath = "/docs" + return config +} + // publicにわける // user(ログイン必須) // private(管理者) @@ -38,25 +57,10 @@ func main() { e := echo.New() // configを初期化 configs.NewAppEnvironment() - config := huma.DefaultConfig(configs.OpenAPITitle, configs.OpenAPIVersion) - // Openapiのserver設定 - config.Servers = []*huma.Server{ - {URL: "http://localhost:6500/api/v1"}, - } - - config.Components.SecuritySchemes = map[string]*huma.SecurityScheme{ - "bearer": { - Type: "http", - Scheme: "bearer", - BearerFormat: "JWT", - }, - } - config.DocsPath = "/docs" // ミドルウェアを適用(すべてのリクエストに対して) middleware.CustomMiddleware(e) - // /api/v1/openapi.yaml // これgroup化したやつをnewUserRouterに渡す必要かも - api = humaecho.NewWithGroup(e, e.Group("/api/v1"), config) + api = humaecho.NewWithGroup(e, e.Group("/api/v1"), initHuma()) router.NewPublicRouter(api) // 未定義のルート用のキャッチオールハンドラ diff --git a/backend/configs/constructor.go b/backend/configs/constructor.go index e64bb9d..2bb1ca4 100644 --- a/backend/configs/constructor.go +++ b/backend/configs/constructor.go @@ -1,7 +1,38 @@ package configs const ( + // ApplicationPort is the port the application listens on. ApplicationPort = ":6500" - OpenAPITitle = "TinyURL" - OpenAPIVersion = "1.0.0" + // OpenAPITitle is the title of the OpenAPI spec. + OpenAPITitle = "TinyURL API" + // OpenAPIVersion is the version of the OpenAPI spec. + OpenAPIVersion = "1.0.0" + // OpenAPIServerPath is the base URL for the OpenAPI spec. + OpenAPIDocServerPath = "http://localhost:6500/api/v1" ) + +// OperationID: このAPI操作の一意の識別子。これは、API内で操作を参照する際に使用されます。 +// Method: HTTPメソッドを指定します。この例では http.MethodGet が使われており、これはHTTPのGETリクエストを示します。 +// Path: エンドポイントのURLパスを指定します。ここでは "/greeting/{name}" となっており、{name} はパスパラメータを表しています。 +// Summary: 短い説明文です。APIのドキュメントに表示され、APIの目的を簡潔に説明します。 +// Description: APIエンドポイントの詳細な説明です。ここでは操作の詳細や動作についての追加情報を提供します。 +// Tags: このAPI操作に関連付けられたタグのリストです。これにより、APIドキュメント内で類似の操作をグループ化することができます。 + +// huma.Register(app, huma.Operation{ +// OperationID: "health", +// Method: http.MethodGet, +// Path: Router.Health, +// Summary: "Health Check", +// Description: "Check the health of the service.", +// Tags: []string{"Public"}, +// }, func(_ context.Context, _ *HealthCheckParams) (*HealthCheckQuery, error) { +// resp := &HealthCheckQuery{ +// Body: struct{ +// Message string `json:"message,omitempty" example:"Hello, world!" doc:"Greeting message"` +// }{ +// Message: "ok", +// }, +// } +// fmt.Printf("Health Check: %v\n", resp.Body.Message) +// return resp, nil +// }) diff --git a/backend/configs/router.go b/backend/configs/router.go index c0f12b7..9640fd0 100644 --- a/backend/configs/router.go +++ b/backend/configs/router.go @@ -8,7 +8,10 @@ package configs type path = string const ( - Health path = "/health" - GetShortURL path = "/api/v1/urls/:shortUrl" - CreateShortURL path = "/api/v1/urls" + // /api/v1/health + Health path = "/health" + // /api/v1/urls + GetShortURL path = "/urls/:id" + // /api/v1/urls + CreateShortURL path = "/urls" ) diff --git a/backend/docker/localstack/init-aws.sh b/backend/docker/localstack/init-aws.sh index 1bdb59d..b417d9e 100755 --- a/backend/docker/localstack/init-aws.sh +++ b/backend/docker/localstack/init-aws.sh @@ -14,8 +14,6 @@ echo "init dynamoDB" awslocal dynamodb create-table --table-name offline-tinyurls \ --attribute-definitions \ AttributeName=id,AttributeType=S \ - AttributeName=originalURL,AttributeType=S \ - AttributeName=createdAt,AttributeType=S \ --key-schema \ AttributeName=id,KeyType=HASH \ --billing-mode PAY_PER_REQUEST diff --git a/backend/domain/shorturl.go b/backend/domain/shorturl.go index 9200c1a..0626fd6 100644 --- a/backend/domain/shorturl.go +++ b/backend/domain/shorturl.go @@ -1,7 +1,25 @@ package domain +import ( + "crypto/sha1" + "encoding/base64" + "strings" + "time" +) + type ShortURL struct { ID string `json:"id"` - OriginalURL string `json:"original"` + OriginalURL string `json:"original_url"` CreatedAt string `json:"created_at"` } + +func GenerateShortURL(originalURL string) *ShortURL { + hasher := sha1.New() + hasher.Write([]byte(originalURL)) + sha := base64.URLEncoding.EncodeToString(hasher.Sum(nil)) + return &ShortURL{ + ID: strings.TrimRight(sha, "=")[:7], + OriginalURL: originalURL, + CreatedAt: time.Now().Format(time.RFC3339), + } +} diff --git a/backend/domain/url.go b/backend/domain/url.go deleted file mode 100644 index 93dd751..0000000 --- a/backend/domain/url.go +++ /dev/null @@ -1,8 +0,0 @@ -package domain - -type URL struct { - ID int `json:"id"` - ShortURL string `json:"short_url"` - OriginalURL string `json:"original"` - CreatedAt string `json:"created_at"` -} diff --git a/backend/go.mod b/backend/go.mod index 5f23673..b85668d 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -7,7 +7,8 @@ require ( github.com/aws/aws-sdk-go-v2/config v1.27.11 github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.13.13 github.com/aws/aws-sdk-go-v2/service/dynamodb v1.31.1 - github.com/danielgtaylor/huma/v2 v2.14.0 + github.com/aws/smithy-go v1.20.2 + github.com/danielgtaylor/huma/v2 v2.15.0 github.com/go-playground/validator/v10 v10.19.0 github.com/kelseyhightower/envconfig v1.4.0 github.com/labstack/echo/v4 v4.11.4 @@ -28,7 +29,6 @@ require ( github.com/aws/aws-sdk-go-v2/service/sso v1.20.5 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 // indirect - github.com/aws/smithy-go v1.20.2 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect diff --git a/backend/go.sum b/backend/go.sum index 584248c..0ada87f 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -33,8 +33,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.28.6/go.mod h1:FZf1/nKNEkHdGGJP/cI2M github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/danielgtaylor/huma/v2 v2.14.0 h1:lRuhQQPZhePvJ4B/m4kfIUYfD8ZPy5BKq/oktLFmB50= -github.com/danielgtaylor/huma/v2 v2.14.0/go.mod h1:OdHC/JliXtOrnvHLQTU5qV7WvYRQXwWY1tkl5rLXmuE= +github.com/danielgtaylor/huma/v2 v2.15.0 h1:26c3hxNT+0xNc8qDLPXNko48qyi31RDFQdhi36gorRI= +github.com/danielgtaylor/huma/v2 v2.15.0/go.mod h1:OdHC/JliXtOrnvHLQTU5qV7WvYRQXwWY1tkl5rLXmuE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/backend/internal/infrastructures/dynamo/dynamo.go b/backend/internal/infrastructures/dynamo/dynamo.go index 3a4584a..5953c9b 100644 --- a/backend/internal/infrastructures/dynamo/dynamo.go +++ b/backend/internal/infrastructures/dynamo/dynamo.go @@ -4,6 +4,7 @@ import ( "context" "log/slog" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/dynamodb" ) @@ -13,7 +14,18 @@ type Connection struct { } func NewDynamoConnection() *Connection { - cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("ap-northeast-1")) + // https://zenn.dev/y16ra/articles/40ff14e8d2a4db + customResolver := aws.EndpointResolverFunc(func(service, region string) (aws.Endpoint, error) { + return aws.Endpoint{ + PartitionID: "aws", + URL: "http://aws:4566", // LocalStackのDynamoDBエンドポイント + SigningRegion: "ap-northeast-1", + }, nil + }) + cfg, err := config.LoadDefaultConfig(context.TODO(), + config.WithRegion("ap-northeast-1"), + config.WithEndpointResolver(customResolver), + ) if err != nil { slog.Error("unable to load SDK config, %v", err) } diff --git a/backend/internal/repository/dynamo/shorturl.go b/backend/internal/repository/dynamo/shorturl.go index 6d2d46c..54ca2fa 100644 --- a/backend/internal/repository/dynamo/shorturl.go +++ b/backend/internal/repository/dynamo/shorturl.go @@ -2,10 +2,12 @@ package dynamo import ( "context" + "errors" "log" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/smithy-go" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" "github.com/naohito-T/tinyurl/backend/domain" @@ -24,7 +26,6 @@ type ShortURLRepository struct { logger slog.ILogger } -// NewShortURLRepository アスタリスク * は、ポインタを通じてポインタが指し示すアドレスに格納されている実際の値にアクセスするために使います。また、型宣言でポインタ型を示す際にも用いられます。 func NewShortURLRepository(client *DynamoClient.Connection, logger slog.ILogger) *ShortURLRepository { // &ShortURLRepository{...} によって ShortURLRepository 型の新しいインスタンスがメモリ上に作成され、そのインスタンスのアドレスが返されます return &ShortURLRepository{ @@ -43,11 +44,12 @@ type ItemKey struct { ID string `dynamodbav:"id"` } -func (r *ShortURLRepository) GetByShortURL(id string) (domain.ShortURL, error) { - r.logger.Debug("GetItemInput: %v", id) +// 構造体に属することで、構造体が初期されていないと呼び出すことはできないことになる。 +func (r *ShortURLRepository) Get(ctx context.Context, hashURL string) (domain.ShortURL, error) { + r.logger.Debug("GetItemInput: %v", hashURL) itemKey := ItemKey{ - ID: id, + ID: hashURL, } av, err := attributevalue.MarshalMap(itemKey) @@ -56,7 +58,7 @@ func (r *ShortURLRepository) GetByShortURL(id string) (domain.ShortURL, error) { return domain.ShortURL{}, err } - result, err := r.Client.Conn.GetItem(context.TODO(), &dynamodb.GetItemInput{ + result, err := r.Client.Conn.GetItem(ctx, &dynamodb.GetItemInput{ TableName: aws.String("offline-tinyurls"), Key: av, }) @@ -82,8 +84,8 @@ func (r *ShortURLRepository) GetByShortURL(id string) (domain.ShortURL, error) { return shortURL, nil } -func (r *ShortURLRepository) CreateShortURL(params *domain.ShortURL) error { - r.logger.Debug("PutItemInput: %v", params) +func (r *ShortURLRepository) Put(ctx context.Context, params *domain.ShortURL) (domain.ShortURL, error) { + r.logger.Info("PutItemInput: %v", params) item := TableItem{ ID: params.ID, @@ -93,15 +95,25 @@ func (r *ShortURLRepository) CreateShortURL(params *domain.ShortURL) error { av, err := attributevalue.MarshalMap(item) if err != nil { log.Fatal(err) + return domain.ShortURL{}, err // エラー時にゼロ値を返す } - _, err = r.Client.Conn.PutItem(context.TODO(), &dynamodb.PutItemInput{ + _, err = r.Client.Conn.PutItem(ctx, &dynamodb.PutItemInput{ TableName: aws.String("offline-tinyurls"), Item: av, }) + var oe *smithy.OperationError + if errors.As(err, &oe) { + log.Printf("failed to call service: %s, operation: %s, error: %v", oe.Service(), oe.Operation(), oe.Unwrap()) + } if err != nil { log.Fatal(err) - return err + return domain.ShortURL{}, err // エラー時にゼロ値を返す } - return nil + shortURL := domain.ShortURL{ + ID: params.ID, + OriginalURL: params.OriginalURL, + CreatedAt: params.CreatedAt, + } + return shortURL, nil } diff --git a/backend/internal/rest/container/container.go b/backend/internal/rest/container/container.go index 67b5d54..5760db6 100644 --- a/backend/internal/rest/container/container.go +++ b/backend/internal/rest/container/container.go @@ -13,7 +13,7 @@ type GuestContainer struct { URLUsecase *usecase.ShortURLUsecase } -var newGuestContainer = sync.OnceValue(func() *GuestContainer { +var onceGuestContainer = sync.OnceValue(func() *GuestContainer { dynamoRepo := repoDynamo.NewShortURLRepository(dynamo.NewDynamoConnection(), slog.NewLogger()) return &GuestContainer{ URLUsecase: usecase.NewURLUsecase(dynamoRepo), @@ -21,5 +21,5 @@ var newGuestContainer = sync.OnceValue(func() *GuestContainer { }) func NewGuestContainer() *GuestContainer { - return newGuestContainer() + return onceGuestContainer() } diff --git a/backend/internal/rest/context/humaecho.go b/backend/internal/rest/context/humaecho.go deleted file mode 100644 index 9a142fb..0000000 --- a/backend/internal/rest/context/humaecho.go +++ /dev/null @@ -1,47 +0,0 @@ -package context - -// package humaecho - -// import ( -// "context" - -// "github.com/danielgtaylor/huma/v2" -// "github.com/labstack/echo/v4" -// ) - -// // EchoHumaContext は huma.Context に echo.Context を追加します。 -// type EchoHumaContext interface { -// // これは継承らしい -// huma.Context -// EchoContext() echo.Context -// } - -// // echoContextImpl は EchoHumaContext の実装です。 -// type echoContextImpl struct { -// humaCtx huma.Context -// echoCtx echo.Context -// } - -// func (e *echoContextImpl) Operation() *huma.Operation { -// return e.humaCtx.Operation() -// } - -// func (e *echoContextImpl) Context() context.Context { -// return e.humaCtx.Context() -// } - -// // 以下、huma.Context の他のメソッドも同様に実装します。 -// // ... - -// // EchoContext は echo.Context を返します。 -// func (e *echoContextImpl) EchoContext() echo.Context { -// return e.echoCtx -// } - -// // NewEchoHumaContext は新しい EchoHumaContext を作成します。 -// func NewEchoHumaContext(hCtx huma.Context, eCtx echo.Context) EchoHumaContext { -// return &echoContextImpl{ -// humaCtx: hCtx, // huma.Context をセット -// echoCtx: eCtx, // echo.Context をセット -// } -// } diff --git a/backend/internal/rest/handler/handler.go b/backend/internal/rest/handler/handler.go deleted file mode 100644 index d080e4d..0000000 --- a/backend/internal/rest/handler/handler.go +++ /dev/null @@ -1,7 +0,0 @@ -package handler - -import "github.com/labstack/echo/v4" - -type Handler interface { - HealthHandler(c echo.Context) error -} diff --git a/backend/internal/rest/handler/shorturl.go b/backend/internal/rest/handler/shorturl.go index b146d42..986f6ab 100644 --- a/backend/internal/rest/handler/shorturl.go +++ b/backend/internal/rest/handler/shorturl.go @@ -1,11 +1,36 @@ package handler import ( - "net/http" + "context" - "github.com/labstack/echo/v4" + "github.com/naohito-T/tinyurl/backend/domain" + "github.com/naohito-T/tinyurl/backend/internal/infrastructures/slog" + "github.com/naohito-T/tinyurl/backend/internal/rest/container" ) -func ShortURLHandler(c echo.Context) error { - return c.String(http.StatusOK, "OK") +type ShortURLHandler struct { + container *container.GuestContainer +} + +// IShortURLHandler defines the interface for short URL handler operations. +type IShortURLHandler interface { + GetShortURLHandler(ctx context.Context, hashID string) (domain.ShortURL, error) + CreateShortURLHandler(ctx context.Context, originalURL string) (domain.ShortURL, error) +} + +// NewShortURLHandler creates a new handler for short URLs. +func NewShortURLHandler(c *container.GuestContainer) IShortURLHandler { + return &ShortURLHandler{ + container: c, + } +} + +func (s *ShortURLHandler) GetShortURLHandler(ctx context.Context, hashID string) (domain.ShortURL, error) { + slog.NewLogger().Info("GetShortURLHandler: %v", hashID) + return s.container.URLUsecase.GetByShortURL(ctx, hashID) +} + +func (s *ShortURLHandler) CreateShortURLHandler(ctx context.Context, originalURL string) (domain.ShortURL, error) { + slog.NewLogger().Info("CreateShortURLHandler: %v", originalURL) + return s.container.URLUsecase.CreateShortURL(ctx, originalURL) } diff --git a/backend/internal/rest/router/public.go b/backend/internal/rest/router/public.go index ecb993d..0fd5f68 100644 --- a/backend/internal/rest/router/public.go +++ b/backend/internal/rest/router/public.go @@ -2,104 +2,115 @@ package router import ( "context" - "errors" - "fmt" "net/http" "github.com/danielgtaylor/huma/v2" - "github.com/labstack/echo/v4" - Router "github.com/naohito-T/tinyurl/backend/configs" + "github.com/naohito-T/tinyurl/backend/configs" "github.com/naohito-T/tinyurl/backend/internal/infrastructures/slog" - // "github.com/naohito-T/tinyurl/backend/internal/rest/container" + "github.com/naohito-T/tinyurl/backend/internal/rest/container" + "github.com/naohito-T/tinyurl/backend/internal/rest/handler" ) -type HealthCheckParams struct { - CheckDB string `json:"check_db"` +type HealthCheckQuery struct { + CheckDB bool `query:"q" doc:"Optional database check parameter"` } -// type GreetingOutput struct { -// Greeting string `json:"greeting"` -// Suffix string `json:"suffix"` -// Length int `json:"length"` -// ContentType string `json:"content_type"` -// Num int `json:"num"` -// } +type HealthCheckResponse struct { + Body struct { + Message string `json:"message"` + } +} -type HealthCheckParams2 struct { +type GetTinyURLQuery struct { + ID string `path:"id" required:"true"` +} + +type GetTinyURLResponse struct { Body struct { - // Message string `json:"message" example:"Hello, world!" doc:"Greeting message"` - Message string `json:"message" example:"Hello, world!" doc:"Greeting message"` + ID string `json:"id" required:"true"` + OriginalURL string `json:"original_url" required:"true"` + CreatedAt string `json:"created_at" required:"true"` } } -// GreetingOutput represents the greeting operation response. -type GreetingOutput3 struct { +type CreateTinyURLBody struct { Body struct { - Message string `json:"message" example:"Hello, world!" doc:"Greeting message"` + URL string `json:"url" required:"true" example:"http://example.com" doc:"URL to shorten"` } } +type CreateTinyURLResponse struct { + Body struct { + ID string `json:"id"` + } +} + +// 今日の課題 +// 1. tinyulrのAPIを作成する(できそう) +// 2. テストを書く +// 3. ドキュメントを書く + // https://tinyurl.com/app/api/url/create" // NewRouter これもシングルトンにした場合の例が気になる func NewPublicRouter(app huma.API) { - // container := container.NewGuestContainer() + h := handler.NewShortURLHandler(container.NewGuestContainer()) + // これ見ていつか修正する https://aws.amazon.com/jp/builders-library/implementing-health-checks/ huma.Register(app, huma.Operation{ OperationID: "health", Method: http.MethodGet, - Path: Router.Health, - Summary: "Get a greeting", - Description: "Get a greeting for a person by name.", - Tags: []string{"Greetings"}, - }, func(_ context.Context, _ *HealthCheckParams) (*HealthCheckParams2, error) { - resp := &HealthCheckParams2{Body: struct { - Message string `json:"message" example:"Hello, world!" doc:"Greeting message"` - }{Message: "ok3"}} - return resp, nil + Path: configs.Health, + Summary: "Health Check", + Description: "Check the health of the service.", + Tags: []string{"Public"}, + }, func(_ context.Context, input *struct { + HealthCheckQuery + }) (*HealthCheckResponse, error) { + slog.NewLogger().Info("Health Check: %v", input.CheckDB) + return &HealthCheckResponse{ + Body: struct { + Message string `json:"message"` + }{ + Message: "ok", + }, + }, nil }) - // Register GET /greeting/{name} huma.Register(app, huma.Operation{ - OperationID: "get-greeting", + OperationID: "tinyurl", Method: http.MethodGet, - Path: "/greeting/{name}", - Summary: "Get a greeting", - Description: "Get a greeting for a person by name.", - Tags: []string{"Greetings"}, - }, func(_ context.Context, input *struct { - Name string `path:"name" maxLength:"30" example:"world" doc:"Name to greet"` - }) (*GreetingOutput3, error) { - resp := &GreetingOutput3{} - resp.Body.Message = fmt.Sprintf("Hello, %s!", input.Name) + Path: configs.GetShortURL, + Summary: "Get a original URL", + Description: "Get a original URL.", + Tags: []string{"Public"}, + }, func(ctx context.Context, query *struct { + GetTinyURLQuery + }) (*GetTinyURLResponse, error) { + resp := &GetTinyURLResponse{} + shortURL, err := h.GetShortURLHandler(ctx, query.ID) + if err != nil { + return nil, err + } + resp.Body.ID = shortURL.ID + resp.Body.OriginalURL = shortURL.OriginalURL + resp.Body.CreatedAt = shortURL.CreatedAt return resp, nil }) - // e.GET(Router.Health, health) - // e.GET(Router.GetShortURL, hello) - // e.POST(Router.CreateShortURL, hello) - -} - -func hello(c echo.Context) error { - // {"time":"2024-04-14T02:16:18.08145333Z","level":"INFO","msg":"Hello, World!"} - slog.NewLogger().Info("Hello, World!") - if err := isValid("hello"); err != nil { - return err - } - return c.String(http.StatusOK, "Hello, World 2!") -} - -func health(c echo.Context) error { - println("Hello, World!") - if err := isValid("hello"); err != nil { - return err - } - return c.String(http.StatusOK, "Hello, World!") -} - -func isValid(txt string) error { - if txt == "" { - return errors.New("Invalid") - } - return nil + huma.Register(app, huma.Operation{ + OperationID: "tinyurl", + Method: http.MethodPost, + Path: configs.CreateShortURL, + Summary: "Create a short URL", + Description: "Create a short URL.", + Tags: []string{"Public"}, + }, func(ctx context.Context, body *CreateTinyURLBody) (*CreateTinyURLResponse, error) { + resp := &CreateTinyURLResponse{} + shortURL, err := h.CreateShortURLHandler(ctx, body.Body.URL) + if err != nil { + return nil, err + } + resp.Body.ID = shortURL.ID + return resp, nil + }) } diff --git a/backend/internal/usecase/shorturl.go b/backend/internal/usecase/shorturl.go index d2d70e6..c26011d 100644 --- a/backend/internal/usecase/shorturl.go +++ b/backend/internal/usecase/shorturl.go @@ -1,15 +1,21 @@ package usecase import ( + "context" + "github.com/naohito-T/tinyurl/backend/domain" + "github.com/naohito-T/tinyurl/backend/internal/infrastructures/slog" ) type IShortURLRepo interface { - GetByShortURL(id string) (domain.ShortURL, error) - CreateShortURL(params *domain.ShortURL) error + Get(ctx context.Context, hashURL string) (domain.ShortURL, error) + Put(ctx context.Context, shortURL *domain.ShortURL) (domain.ShortURL, error) } type ShortURLUsecase struct { + // ここでIShortURLRepoを埋め込むことで、UsecaseがRepositoryを知っている + // はい、その通りです。IShortURLRepo インターフェースは、Get と Create という二つのメソッドを定義しており、このインターフェースを実装するどのクラスも、これらのメソッドを具体的に実装する必要があります。そして、ShortURLUsecase の中で shortURLRepo.Create(originalURL) を呼び出すことによって、このインターフェースを満たす具体的な実装に対して処理を委譲しています。 + // ここでのポイントは、ShortURLUsecase クラスが IShortURLRepo インターフェースの具体的な実装に依存していないということです。この設計により、IShortURLRepo の実装を変更しても、ShortURLUsecase クラスを修正する必要がなくなります。つまり、データアクセス層の実装が変わっても、ビジネスロジック層は影響を受けないという設計原則(オープン/クローズド原則)に従っています。 shortURLRepo IShortURLRepo } @@ -19,14 +25,12 @@ func NewURLUsecase(u IShortURLRepo) *ShortURLUsecase { } } -func (u *ShortURLUsecase) GetByShortURL(id string) domain.ShortURL { - shortURL, err := u.shortURLRepo.GetByShortURL(id) - if err != nil { - panic(err) - } - return shortURL +func (u *ShortURLUsecase) GetByShortURL(ctx context.Context, hashID string) (domain.ShortURL, error) { + slog.NewLogger().Info("GetByShortURL: %v", hashID) + return u.shortURLRepo.Get(ctx, hashID) } -func (u *ShortURLUsecase) CreateShortURL(params *domain.ShortURL) error { - return u.shortURLRepo.CreateShortURL(params) +func (u *ShortURLUsecase) CreateShortURL(ctx context.Context, originalURL string) (domain.ShortURL, error) { + slog.NewLogger().Info("CreateShortURL: %v", originalURL) + return u.shortURLRepo.Put(ctx, domain.GenerateShortURL(originalURL)) } diff --git a/docker-compose.yml b/docker-compose.yml index 1eb7e24..e807e9d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,8 +7,9 @@ services: environment: SERVICES: dynamodb,s3 DEFAULT_REGION: ap-northeast-1 + TZ: UTC volumes: - # ready hook + # ready hook - "$PWD/backend/docker/localstack/init-aws.sh:/etc/localstack/init/ready.d/init-aws.sh" - 'localstack-data:/tmp/localstack' @@ -20,6 +21,7 @@ services: environment: AWS_ACCESS_KEY_ID: "XXXX" AWS_SECRET_ACCESS_KEY: "XXXX" + TZ: UTC volumes: - $PWD/backend:/app env_file: