diff --git a/api/v1/error.go b/api/v1/error.go deleted file mode 100644 index d823379..0000000 --- a/api/v1/error.go +++ /dev/null @@ -1,11 +0,0 @@ -package v1 - -import "net/http" - -var ErrorCodeMapping = map[ErrorCode]int{ - ParameterInvalid: http.StatusBadRequest, - ParameterMissing: http.StatusBadRequest, - ProcessingError: http.StatusInternalServerError, - ResourceAlreadyExists: http.StatusConflict, - ResourceMissing: http.StatusNotFound, -} diff --git a/api/v1/item.go b/api/v1/item.go deleted file mode 100644 index b59f5c5..0000000 --- a/api/v1/item.go +++ /dev/null @@ -1,40 +0,0 @@ -package v1 - -import ( - "github.com/glass-cms/glasscms/item" - "github.com/glass-cms/glasscms/parser" -) - -// ToItem converts an api.ItemCreate to an item.Item. -func (i *ItemCreate) ToItem() *item.Item { - if i == nil { - return nil - } - - return &item.Item{ - Name: i.Name, - DisplayName: i.DisplayName, - Content: i.Content, - Hash: parser.HashContent([]byte(i.Content)), - CreateTime: i.CreateTime, - UpdateTime: i.UpdateTime, - Properties: i.Properties, - Metadata: i.Metadata, - } -} - -func FromItem(item *item.Item) *Item { - if item == nil { - return nil - } - - return &Item{ - Name: item.Name, - DisplayName: item.DisplayName, - Content: item.Content, - CreateTime: item.CreateTime, - UpdateTime: item.UpdateTime, - Properties: item.Properties, - Metadata: item.Metadata, - } -} diff --git a/api/v1/item_test.go b/api/v1/item_test.go deleted file mode 100644 index f6ab786..0000000 --- a/api/v1/item_test.go +++ /dev/null @@ -1,121 +0,0 @@ -package v1_test - -import ( - "testing" - "time" - - v1 "github.com/glass-cms/glasscms/api/v1" - "github.com/glass-cms/glasscms/item" - "github.com/glass-cms/glasscms/parser" - "github.com/stretchr/testify/assert" -) - -func TestItem_MapToDomain(t *testing.T) { - t.Parallel() - - type fields struct { - Content string - CreateTime time.Time - DisplayName string - ID string - Name string - Properties map[string]interface{} - UpdateTime time.Time - } - - tests := []struct { - name string - fields fields - want *item.Item - }{ - { - name: "Complete Item Mapping", - fields: fields{ - Content: "Test Content", - CreateTime: time.Now().Add(-24 * time.Hour), - DisplayName: "Test Display Name", - ID: "1234", - Name: "Test Name", - Properties: map[string]interface{}{"key1": "value1", "key2": "value2"}, - UpdateTime: time.Now(), - }, - want: &item.Item{ - Name: "Test Name", - DisplayName: "Test Display Name", - Content: "Test Content", - Hash: parser.HashContent([]byte("Test Content")), - CreateTime: time.Now().Add(-24 * time.Hour), - UpdateTime: time.Now(), - Properties: map[string]any{"key1": "value1", "key2": "value2"}, - }, - }, - { - name: "Empty Item Mapping", - fields: fields{ - Content: "", - CreateTime: time.Time{}, - DisplayName: "", - ID: "", - Name: "", - Properties: nil, - UpdateTime: time.Time{}, - }, - want: &item.Item{ - Name: "", - DisplayName: "", - Content: "", - Hash: parser.HashContent([]byte("")), - CreateTime: time.Time{}, - UpdateTime: time.Time{}, - Properties: nil, - }, - }, - { - name: "Nil Properties Mapping", - fields: fields{ - Content: "Test Content", - CreateTime: time.Now().Add(-24 * time.Hour), - DisplayName: "Test Display Name", - ID: "5678", - Name: "Test Name 2", - Properties: nil, - UpdateTime: time.Now(), - }, - want: &item.Item{ - Name: "Test Name 2", - Content: "Test Content", - DisplayName: "Test Display Name", - Hash: parser.HashContent([]byte("Test Content")), - CreateTime: time.Now().Add(-24 * time.Hour), - UpdateTime: time.Now(), - Properties: nil, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - apiItem := &v1.ItemCreate{ - Content: tt.fields.Content, - CreateTime: tt.fields.CreateTime, - DisplayName: tt.fields.DisplayName, - Name: tt.fields.Name, - Properties: tt.fields.Properties, - UpdateTime: tt.fields.UpdateTime, - } - got := apiItem.ToItem() - - // Adjust for potential differences in time (e.g., slight differences due to test execution timing) - if !got.CreateTime.IsZero() && !tt.want.CreateTime.IsZero() { - got.CreateTime = tt.want.CreateTime - } - if !got.UpdateTime.IsZero() && !tt.want.UpdateTime.IsZero() { - got.UpdateTime = tt.want.UpdateTime - } - - assert.Equal(t, tt.want, got) - }) - } -} diff --git a/cmd/convert.go b/cmd/convert.go index 111a723..eec9fa7 100644 --- a/cmd/convert.go +++ b/cmd/convert.go @@ -8,9 +8,9 @@ import ( "os" "path" - "github.com/glass-cms/glasscms/item" - "github.com/glass-cms/glasscms/parser" - "github.com/glass-cms/glasscms/sourcer" + "github.com/glass-cms/glasscms/internal/item" + "github.com/glass-cms/glasscms/internal/parser" + "github.com/glass-cms/glasscms/internal/sourcer" "github.com/lmittmann/tint" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/migrate.go b/cmd/migrate.go index b01d7bf..a1c524b 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -4,7 +4,7 @@ import ( "log/slog" "os" - "github.com/glass-cms/glasscms/database" + "github.com/glass-cms/glasscms/internal/database" "github.com/lmittmann/tint" "github.com/spf13/cobra" "github.com/spf13/viper" diff --git a/cmd/server/server.go b/cmd/server/server.go index 6e3618b..6273db2 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -1,7 +1,7 @@ package server import ( - "github.com/glass-cms/glasscms/database" + "github.com/glass-cms/glasscms/internal/database" "github.com/spf13/cobra" ) diff --git a/cmd/server/start.go b/cmd/server/start.go index 08c4b07..06808d3 100644 --- a/cmd/server/start.go +++ b/cmd/server/start.go @@ -6,12 +6,10 @@ import ( "os" "os/user" - "github.com/glass-cms/glasscms/database" - "github.com/glass-cms/glasscms/item" - ctx "github.com/glass-cms/glasscms/lib/context" - "github.com/glass-cms/glasscms/server" - "github.com/glass-cms/glasscms/server/handler" - v1 "github.com/glass-cms/glasscms/server/handler/v1" + "github.com/glass-cms/glasscms/internal/database" + "github.com/glass-cms/glasscms/internal/item" + "github.com/glass-cms/glasscms/internal/server" + ctx "github.com/glass-cms/glasscms/pkg/context" "github.com/lmittmann/tint" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -95,9 +93,8 @@ func (c *StartCommand) Execute(cmd *cobra.Command, _ []string) error { repo := item.NewRepository(db, errHandler) service := item.NewService(repo) - v1Handler := v1.NewAPIHandler(c.logger, service) - server, err := server.New(c.logger, []handler.VersionedHandler{v1Handler}) + server, err := server.New(c.logger, service) if err != nil { return err } diff --git a/generate.go b/generate.go index f5d43e7..0ed89ea 100644 --- a/generate.go +++ b/generate.go @@ -1,3 +1,3 @@ package main -//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=openapi/codegen.cfg.v1.yaml openapi/openapi.v1.yaml +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=oapi-codegen-config.yaml openapi.yaml diff --git a/database/config.go b/internal/database/config.go similarity index 100% rename from database/config.go rename to internal/database/config.go diff --git a/database/error.go b/internal/database/error.go similarity index 100% rename from database/error.go rename to internal/database/error.go diff --git a/database/error_test.go b/internal/database/error_test.go similarity index 95% rename from database/error_test.go rename to internal/database/error_test.go index 545ecbc..0f57d0c 100644 --- a/database/error_test.go +++ b/internal/database/error_test.go @@ -6,7 +6,7 @@ import ( "errors" "testing" - "github.com/glass-cms/glasscms/database" + "github.com/glass-cms/glasscms/internal/database" "github.com/mattn/go-sqlite3" "github.com/stretchr/testify/assert" ) diff --git a/database/migrate.go b/internal/database/migrate.go similarity index 100% rename from database/migrate.go rename to internal/database/migrate.go diff --git a/database/migrations/1_create_items.sql b/internal/database/migrations/1_create_items.sql similarity index 100% rename from database/migrations/1_create_items.sql rename to internal/database/migrations/1_create_items.sql diff --git a/lib/test/database.go b/internal/database/test.go similarity index 53% rename from lib/test/database.go rename to internal/database/test.go index d485601..03bbdea 100644 --- a/lib/test/database.go +++ b/internal/database/test.go @@ -1,27 +1,26 @@ -package test +package database import ( "database/sql" "fmt" - "github.com/glass-cms/glasscms/database" "github.com/google/uuid" ) // NewDB creates a new in-memory SQLite database with the necessary schema // for testing purposes. -func NewDB() (*sql.DB, error) { - config := database.Config{ - Driver: database.DriverName[int32(database.DriverSqlite)], +func NewTestDB() (*sql.DB, error) { + config := Config{ + Driver: DriverName[int32(DriverSqlite)], DSN: fmt.Sprintf("file:%s?mode=memory&cache=shared", uuid.New().String()), } - db, err := database.NewConnection(config) + db, err := NewConnection(config) if err != nil { return nil, err } - if err = database.MigrateDatabase(db, config); err != nil { + if err = MigrateDatabase(db, config); err != nil { return nil, err } diff --git a/item/item.go b/internal/item/item.go similarity index 100% rename from item/item.go rename to internal/item/item.go diff --git a/item/repository.go b/internal/item/repository.go similarity index 98% rename from item/repository.go rename to internal/item/repository.go index 7cc8b1a..b26963e 100644 --- a/item/repository.go +++ b/internal/item/repository.go @@ -7,7 +7,7 @@ import ( "errors" "fmt" - "github.com/glass-cms/glasscms/database" + "github.com/glass-cms/glasscms/internal/database" ) type Repository interface { diff --git a/item/repository_test.go b/internal/item/repository_test.go similarity index 98% rename from item/repository_test.go rename to internal/item/repository_test.go index 3e81272..93360c5 100644 --- a/item/repository_test.go +++ b/internal/item/repository_test.go @@ -6,8 +6,8 @@ import ( "testing" "time" - "github.com/glass-cms/glasscms/database" - "github.com/glass-cms/glasscms/item" + "github.com/glass-cms/glasscms/internal/database" + "github.com/glass-cms/glasscms/internal/item" _ "github.com/mattn/go-sqlite3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/item/service.go b/internal/item/service.go similarity index 89% rename from item/service.go rename to internal/item/service.go index 74f97bd..8cb99db 100644 --- a/item/service.go +++ b/internal/item/service.go @@ -4,8 +4,8 @@ import ( "context" "errors" - "github.com/glass-cms/glasscms/database" - "github.com/glass-cms/glasscms/lib/resource" + "github.com/glass-cms/glasscms/internal/database" + "github.com/glass-cms/glasscms/pkg/resource" ) // Service is a service for managing items. diff --git a/parser/parser.go b/internal/parser/parser.go similarity index 95% rename from parser/parser.go rename to internal/parser/parser.go index 28fc216..fc7e0d4 100644 --- a/parser/parser.go +++ b/internal/parser/parser.go @@ -9,8 +9,8 @@ import ( "path/filepath" "strings" - "github.com/glass-cms/glasscms/item" - "github.com/glass-cms/glasscms/sourcer" + "github.com/glass-cms/glasscms/internal/item" + "github.com/glass-cms/glasscms/internal/sourcer" "github.com/mozillazg/go-slugify" "gopkg.in/yaml.v3" ) diff --git a/parser/parser_test.go b/internal/parser/parser_test.go similarity index 95% rename from parser/parser_test.go rename to internal/parser/parser_test.go index 175078f..93fdae3 100644 --- a/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/glass-cms/glasscms/parser" + "github.com/glass-cms/glasscms/internal/parser" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/server/handler/v1/error_handler.go b/internal/server/error_handler.go similarity index 72% rename from server/handler/v1/error_handler.go rename to internal/server/error_handler.go index d71b604..b44cdc5 100644 --- a/server/handler/v1/error_handler.go +++ b/internal/server/error_handler.go @@ -1,15 +1,14 @@ -package v1 +package server import ( "net/http" "reflect" - v1 "github.com/glass-cms/glasscms/api/v1" - "github.com/glass-cms/glasscms/server/handler" + "github.com/glass-cms/glasscms/pkg/api" ) // ErrorMapper is a function that maps an error to an API error response. -type ErrorMapper func(error) *v1.Error +type ErrorMapper func(error) *api.Error type ErrorHandler struct { Mappers map[reflect.Type]ErrorMapper @@ -37,20 +36,20 @@ func (h *ErrorHandler) HandleError(w http.ResponseWriter, r *http.Request, err e if mapper, exists := h.Mappers[errType]; exists { errResp := mapper(err) - statusCode, ok := v1.ErrorCodeMapping[errResp.Code] + statusCode, ok := ErrorCodeMapping[errResp.Code] if !ok { statusCode = http.StatusInternalServerError } - handler.SerializeResponse(w, r, statusCode, errResp) + SerializeResponse(w, r, statusCode, errResp) return } // Fallback on generic error response if we don't have a specific error mapper. - errResp := &v1.Error{ - Code: v1.ProcessingError, + errResp := &api.Error{ + Code: api.ProcessingError, Message: "An error occurred while processing the request.", - Type: v1.ApiError, + Type: api.ApiError, } - handler.SerializeResponse(w, r, http.StatusInternalServerError, errResp) + SerializeResponse(w, r, http.StatusInternalServerError, errResp) } diff --git a/server/handler/v1/error_handler_test.go b/internal/server/error_handler_test.go similarity index 91% rename from server/handler/v1/error_handler_test.go rename to internal/server/error_handler_test.go index 4226d8f..fd7fcf1 100644 --- a/server/handler/v1/error_handler_test.go +++ b/internal/server/error_handler_test.go @@ -1,4 +1,4 @@ -package v1_test +package server_test import ( "encoding/json" @@ -8,15 +8,15 @@ import ( "reflect" "testing" - api "github.com/glass-cms/glasscms/api/v1" - "github.com/glass-cms/glasscms/lib/resource" - v1 "github.com/glass-cms/glasscms/server/handler/v1" + "github.com/glass-cms/glasscms/internal/server" + "github.com/glass-cms/glasscms/pkg/api" + "github.com/glass-cms/glasscms/pkg/resource" ) func TestErrorHandler_HandleError(t *testing.T) { t.Parallel() - errorHandler := v1.NewErrorHandler() + errorHandler := server.NewErrorHandler() errorHandler.RegisterErrorMapper(reflect.TypeOf(&resource.AlreadyExistsError{}), func(_ error) *api.Error { return &api.Error{ Code: api.ResourceAlreadyExists, diff --git a/server/handler/v1/errors.go b/internal/server/errors.go similarity index 54% rename from server/handler/v1/errors.go rename to internal/server/errors.go index b323ebc..e5ed6c9 100644 --- a/server/handler/v1/errors.go +++ b/internal/server/errors.go @@ -1,24 +1,25 @@ -package v1 +package server import ( "errors" "fmt" + "net/http" - v1 "github.com/glass-cms/glasscms/api/v1" - "github.com/glass-cms/glasscms/lib/resource" + "github.com/glass-cms/glasscms/pkg/api" + "github.com/glass-cms/glasscms/pkg/resource" ) // ErrorMapperAlreadyExistsError maps a resource.AlreadyExistsError to an API error response. -func ErrorMapperAlreadyExistsError(err error) *v1.Error { +func ErrorMapperAlreadyExistsError(err error) *api.Error { var alreadyExistsErr *resource.AlreadyExistsError if !errors.As(err, &alreadyExistsErr) { panic("error is not a resource.AlreadyExistsError") } - return &v1.Error{ - Code: v1.ResourceAlreadyExists, + return &api.Error{ + Code: api.ResourceAlreadyExists, Message: fmt.Sprintf("An %s with the name already exists", alreadyExistsErr.Resource), - Type: v1.ApiError, + Type: api.ApiError, Details: map[string]interface{}{ "resource": alreadyExistsErr.Resource, "name": alreadyExistsErr.Name, @@ -26,19 +27,27 @@ func ErrorMapperAlreadyExistsError(err error) *v1.Error { } } -func ErrorMapperNotFoundError(err error) *v1.Error { +func ErrorMapperNotFoundError(err error) *api.Error { var notFoundErr *resource.NotFoundError if !errors.As(err, ¬FoundErr) { panic("error is not a resource.NotFoundError") } - return &v1.Error{ - Code: v1.ResourceMissing, + return &api.Error{ + Code: api.ResourceMissing, Message: fmt.Sprintf("The %s with the name was not found", notFoundErr.Resource), - Type: v1.ApiError, + Type: api.ApiError, Details: map[string]interface{}{ "resource": notFoundErr.Resource, "name": notFoundErr.Name, }, } } + +var ErrorCodeMapping = map[api.ErrorCode]int{ + api.ParameterInvalid: http.StatusBadRequest, + api.ParameterMissing: http.StatusBadRequest, + api.ProcessingError: http.StatusInternalServerError, + api.ResourceAlreadyExists: http.StatusConflict, + api.ResourceMissing: http.StatusNotFound, +} diff --git a/server/handler/v1/errors_test.go b/internal/server/errors_test.go similarity index 72% rename from server/handler/v1/errors_test.go rename to internal/server/errors_test.go index 6de00b0..e78b279 100644 --- a/server/handler/v1/errors_test.go +++ b/internal/server/errors_test.go @@ -1,12 +1,12 @@ -package v1_test +package server_test import ( "errors" "testing" - v1 "github.com/glass-cms/glasscms/api/v1" - "github.com/glass-cms/glasscms/lib/resource" - v1_handler "github.com/glass-cms/glasscms/server/handler/v1" + "github.com/glass-cms/glasscms/internal/server" + "github.com/glass-cms/glasscms/pkg/api" + "github.com/glass-cms/glasscms/pkg/resource" "github.com/stretchr/testify/require" ) @@ -18,17 +18,17 @@ func TestErrorMapperAlreadyExistsError(t *testing.T) { } tests := map[string]struct { args args - want *v1.Error + want *api.Error expectPanics bool }{ "maps resource.AlreadyExistsError to an API error response": { args: args{ err: resource.NewAlreadyExistsError("item1", "item", errors.New("underlying error")), }, - want: &v1.Error{ - Code: v1.ResourceAlreadyExists, + want: &api.Error{ + Code: api.ResourceAlreadyExists, Message: "An item with the name already exists", - Type: v1.ApiError, + Type: api.ApiError, Details: map[string]interface{}{ "resource": "item", "name": "item1", @@ -48,12 +48,12 @@ func TestErrorMapperAlreadyExistsError(t *testing.T) { if tt.expectPanics { require.Panics(t, func() { - v1_handler.ErrorMapperAlreadyExistsError(tt.args.err) + server.ErrorMapperAlreadyExistsError(tt.args.err) }) return } - require.Equal(t, tt.want, v1_handler.ErrorMapperAlreadyExistsError(tt.args.err)) + require.Equal(t, tt.want, server.ErrorMapperAlreadyExistsError(tt.args.err)) }) } } @@ -66,17 +66,17 @@ func TestErrorMapperNotFoundError(t *testing.T) { } tests := map[string]struct { args args - want *v1.Error + want *api.Error expectPanics bool }{ "maps resource.NotFoundError to an API error response": { args: args{ err: resource.NewNotFoundError("item1", "item", errors.New("underlying error")), }, - want: &v1.Error{ - Code: v1.ResourceMissing, + want: &api.Error{ + Code: api.ResourceMissing, Message: "The item with the name was not found", - Type: v1.ApiError, + Type: api.ApiError, Details: map[string]interface{}{ "resource": "item", "name": "item1", @@ -96,12 +96,12 @@ func TestErrorMapperNotFoundError(t *testing.T) { if tt.expectPanics { require.Panics(t, func() { - v1_handler.ErrorMapperNotFoundError(tt.args.err) + server.ErrorMapperNotFoundError(tt.args.err) }) return } - require.Equal(t, tt.want, v1_handler.ErrorMapperNotFoundError(tt.args.err)) + require.Equal(t, tt.want, server.ErrorMapperNotFoundError(tt.args.err)) }) } } diff --git a/internal/server/item.go b/internal/server/item.go new file mode 100644 index 0000000..5f3b1b7 --- /dev/null +++ b/internal/server/item.go @@ -0,0 +1,88 @@ +package server + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/glass-cms/glasscms/internal/item" + "github.com/glass-cms/glasscms/internal/parser" + "github.com/glass-cms/glasscms/pkg/api" +) + +func (s *Server) ItemsCreate(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + reqBody, err := io.ReadAll(r.Body) + if err != nil { + s.logger.ErrorContext(ctx, fmt.Errorf("failed to read request body: %w", err).Error()) + s.errorHandler.HandleError(w, r, err) + return + } + + var request *api.ItemsCreateJSONRequestBody + if err = json.Unmarshal(reqBody, &request); err != nil { + s.logger.ErrorContext(ctx, fmt.Errorf("failed to unmarshal request body: %w", err).Error()) + s.errorHandler.HandleError(w, r, err) + return + } + + err = s.itemService.CreateItem(ctx, ToItem(request)) + if err != nil { + s.logger.ErrorContext(ctx, fmt.Errorf("failed to create item: %w", err).Error()) + s.errorHandler.HandleError(w, r, err) + return + } + + // TODO: Write response. + + w.WriteHeader(http.StatusCreated) +} + +func (s *Server) ItemsGet(w http.ResponseWriter, r *http.Request, name api.ItemKey) { + ctx := r.Context() + + item, err := s.itemService.GetItem(ctx, name) + if err != nil { + s.logger.ErrorContext(ctx, fmt.Errorf("failed to get item: %w", err).Error()) + s.errorHandler.HandleError(w, r, err) + return + } + + SerializeResponse(w, r, http.StatusOK, item) +} + +// ToItem converts an api.ItemCreate to an item.Item. +func ToItem(i *api.ItemCreate) *item.Item { + if i == nil { + return nil + } + + return &item.Item{ + Name: i.Name, + DisplayName: i.DisplayName, + Content: i.Content, + Hash: parser.HashContent([]byte(i.Content)), + CreateTime: i.CreateTime, + UpdateTime: i.UpdateTime, + Properties: i.Properties, + Metadata: i.Metadata, + } +} + +func FromItem(item *item.Item) *api.Item { + if item == nil { + return nil + } + + return &api.Item{ + Name: item.Name, + DisplayName: item.DisplayName, + Content: item.Content, + CreateTime: item.CreateTime, + UpdateTime: item.UpdateTime, + Properties: item.Properties, + Metadata: item.Metadata, + } +} diff --git a/server/handler/v1/item_test.go b/internal/server/item_test.go similarity index 80% rename from server/handler/v1/item_test.go rename to internal/server/item_test.go index fd9ef35..eaf3aa4 100644 --- a/server/handler/v1/item_test.go +++ b/internal/server/item_test.go @@ -1,4 +1,4 @@ -package v1_test +package server_test import ( "bytes" @@ -7,12 +7,11 @@ import ( "net/http/httptest" "testing" - api "github.com/glass-cms/glasscms/api/v1" - "github.com/glass-cms/glasscms/database" - "github.com/glass-cms/glasscms/item" - "github.com/glass-cms/glasscms/lib/log" - "github.com/glass-cms/glasscms/lib/test" - v1 "github.com/glass-cms/glasscms/server/handler/v1" + "github.com/glass-cms/glasscms/internal/database" + "github.com/glass-cms/glasscms/internal/item" + "github.com/glass-cms/glasscms/internal/server" + "github.com/glass-cms/glasscms/pkg/api" + "github.com/glass-cms/glasscms/pkg/log" "github.com/stretchr/testify/assert" ) @@ -46,7 +45,7 @@ func TestAPIHandler_ItemsCreate(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - testdb, err := test.NewDB() + testdb, err := database.NewTestDB() if err != nil { t.Fatal(err) } @@ -54,10 +53,14 @@ func TestAPIHandler_ItemsCreate(t *testing.T) { repo := item.NewRepository(testdb, &database.SqliteErrorHandler{}) - handler := v1.NewAPIHandler( + handler, err := server.New( log.NoopLogger(), item.NewService(repo), ) + if err != nil { + t.Fatal(err) + return + } rr := httptest.NewRecorder() request := tt.req() @@ -92,7 +95,7 @@ func TestAPIHandler_ItemsGet(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - testdb, err := test.NewDB() + testdb, err := database.NewTestDB() if err != nil { t.Fatal(err) } @@ -100,17 +103,21 @@ func TestAPIHandler_ItemsGet(t *testing.T) { repo := item.NewRepository(testdb, &database.SqliteErrorHandler{}) - handler := v1.NewAPIHandler( + server, err := server.New( log.NoopLogger(), item.NewService(repo), - ).Handler(http.NewServeMux(), []func(http.Handler) http.Handler{}) + ) + if err != nil { + t.Fatal(err) + return + } rr := httptest.NewRecorder() request := tt.req() request.Header.Set("Accept", "application/json") // Make the request - handler.ServeHTTP(rr, request) + server.Handler().ServeHTTP(rr, request) // Assert assert.Equal(t, tt.expected, rr.Code) diff --git a/server/options.go b/internal/server/options.go similarity index 100% rename from server/options.go rename to internal/server/options.go diff --git a/server/handler/serialize.go b/internal/server/serialize.go similarity index 93% rename from server/handler/serialize.go rename to internal/server/serialize.go index 5f0023d..5ecc518 100644 --- a/server/handler/serialize.go +++ b/internal/server/serialize.go @@ -1,11 +1,11 @@ -package handler +package server import ( "encoding/json" "encoding/xml" "net/http" - "github.com/glass-cms/glasscms/lib/mediatype" + "github.com/glass-cms/glasscms/pkg/mediatype" ) // SerializeResponse writes the provided data to the response writer in the diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..e5520d8 --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,114 @@ +package server + +import ( + "context" + "fmt" + "log/slog" + "net/http" + "reflect" + "time" + + "github.com/glass-cms/glasscms/internal/item" + "github.com/glass-cms/glasscms/pkg/api" + "github.com/glass-cms/glasscms/pkg/mediatype" + "github.com/glass-cms/glasscms/pkg/middleware" + "github.com/glass-cms/glasscms/pkg/resource" +) + +const ( + ShutdownGracePeriod = 10 * time.Second + DefaultPort = 8080 + DefaultReadTimeout = 5 * time.Second + DefaultWriteTimeout = 10 * time.Second +) + +var _ api.ServerInterface = (*Server)(nil) + +type Server struct { + logger *slog.Logger + server *http.Server + + itemService *item.Service + errorHandler *ErrorHandler + + handler http.Handler +} + +func New( + logger *slog.Logger, + itemService *item.Service, + opts ...Option, +) (*Server, error) { + serveMux := http.NewServeMux() + + server := &Server{ + logger: logger, + itemService: itemService, + errorHandler: NewErrorHandler(), + } + + middlewares := []func(http.Handler) http.Handler{ + middleware.ContentType(mediatype.ApplicationJSON), + middleware.Accept(mediatype.ApplicationJSON), + } + convertedMiddlewares := make([]api.MiddlewareFunc, len(middlewares)) + for i, mw := range middlewares { + convertedMiddlewares[i] = api.MiddlewareFunc(mw) + } + + server.handler = api.HandlerWithOptions(server, api.StdHTTPServerOptions{ + BaseURL: "", + BaseRouter: serveMux, + }) + + server.server = &http.Server{ + Handler: server.handler, + Addr: fmt.Sprintf(":%v", DefaultPort), + ReadTimeout: DefaultReadTimeout, + WriteTimeout: DefaultWriteTimeout, + } + + for _, opt := range opts { + if err := opt(server); err != nil { + return nil, err + } + } + + server.registerErrorMappers() + return server, nil +} + +// ListenAndServe starts the server. +func (s *Server) ListenAndServer() error { + s.logger.Info("server is listening on :8080") + return s.server.ListenAndServe() +} + +// Shutdown gracefully shuts down the underlying server without interrupting any active connections. +func (s *Server) Shutdown() { + ctx, cancel := context.WithTimeout(context.Background(), ShutdownGracePeriod) + defer cancel() + + if err := s.server.Shutdown(ctx); err != nil { + s.logger.Error("could not gracefully shutdown the server:", "err", err) + return + } + + s.logger.Info("server stopped") +} + +func (s *Server) Handler() http.Handler { + return s.handler +} + +func (s *Server) registerErrorMappers() { + s.errorHandler.RegisterErrorMapper( + reflect.TypeOf(&resource.AlreadyExistsError{}), + ErrorMapperAlreadyExistsError, + ) + + s.errorHandler.RegisterErrorMapper( + reflect.TypeOf(&resource.NotFoundError{}), + ErrorMapperNotFoundError, + ) +} diff --git a/sourcer/file_source.go b/internal/sourcer/file_source.go similarity index 100% rename from sourcer/file_source.go rename to internal/sourcer/file_source.go diff --git a/sourcer/file_source_test.go b/internal/sourcer/file_source_test.go similarity index 94% rename from sourcer/file_source_test.go rename to internal/sourcer/file_source_test.go index 6981846..de8fbf6 100644 --- a/sourcer/file_source_test.go +++ b/internal/sourcer/file_source_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/djherbis/times" - "github.com/glass-cms/glasscms/sourcer" + "github.com/glass-cms/glasscms/internal/sourcer" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/sourcer/file_system.go b/internal/sourcer/file_system.go similarity index 100% rename from sourcer/file_system.go rename to internal/sourcer/file_system.go diff --git a/sourcer/file_system_test.go b/internal/sourcer/file_system_test.go similarity index 98% rename from sourcer/file_system_test.go rename to internal/sourcer/file_system_test.go index c495aee..d91077c 100644 --- a/sourcer/file_system_test.go +++ b/internal/sourcer/file_system_test.go @@ -7,7 +7,7 @@ import ( "path/filepath" "testing" - "github.com/glass-cms/glasscms/sourcer" + "github.com/glass-cms/glasscms/internal/sourcer" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/sourcer/nil_source.go b/internal/sourcer/nil_source.go similarity index 100% rename from sourcer/nil_source.go rename to internal/sourcer/nil_source.go diff --git a/sourcer/source.go b/internal/sourcer/source.go similarity index 100% rename from sourcer/source.go rename to internal/sourcer/source.go diff --git a/sourcer/sourcer.go b/internal/sourcer/sourcer.go similarity index 100% rename from sourcer/sourcer.go rename to internal/sourcer/sourcer.go diff --git a/lib/README.md b/lib/README.md deleted file mode 100644 index 44edbc3..0000000 --- a/lib/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## Libs - -The libs folder contains modules and files that are auxiliary to the application. Libraries contain utility functions that are non-specific to the application domain. They are used to encapsulate code that is used in multiple places in the application. The libs folder is not a place for domain-specific code. \ No newline at end of file diff --git a/openapi/codegen.cfg.v1.yaml b/oapi-codegen-config.yaml similarity index 55% rename from openapi/codegen.cfg.v1.yaml rename to oapi-codegen-config.yaml index 967befe..7dcda91 100644 --- a/openapi/codegen.cfg.v1.yaml +++ b/oapi-codegen-config.yaml @@ -1,5 +1,5 @@ -package: v1 +package: api generate: std-http-server: true models: true -output: api/v1/api.gen.go \ No newline at end of file +output: pkg/api/api.gen.go \ No newline at end of file diff --git a/openapi/openapi.v1.yaml b/openapi.yaml similarity index 99% rename from openapi/openapi.v1.yaml rename to openapi.yaml index 6d56859..c804cf8 100644 --- a/openapi/openapi.v1.yaml +++ b/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.0.0 info: title: GlassCMS API - version: v1 + version: 0.0.0 tags: [] paths: /items: diff --git a/api/v1/api.gen.go b/pkg/api/api.gen.go similarity index 98% rename from api/v1/api.gen.go rename to pkg/api/api.gen.go index 235bbdb..f5a561d 100644 --- a/api/v1/api.gen.go +++ b/pkg/api/api.gen.go @@ -1,9 +1,9 @@ //go:build go1.22 -// Package v1 provides primitives to interact with the openapi HTTP API. +// Package api provides primitives to interact with the openapi HTTP API. // // Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.3.0 DO NOT EDIT. -package v1 +package api import ( "fmt" diff --git a/lib/context/sigterm.go b/pkg/context/sigterm.go similarity index 100% rename from lib/context/sigterm.go rename to pkg/context/sigterm.go diff --git a/lib/log/noop.go b/pkg/log/noop.go similarity index 100% rename from lib/log/noop.go rename to pkg/log/noop.go diff --git a/lib/mediatype/application.go b/pkg/mediatype/application.go similarity index 100% rename from lib/mediatype/application.go rename to pkg/mediatype/application.go diff --git a/lib/mediatype/mediatype.go b/pkg/mediatype/mediatype.go similarity index 100% rename from lib/mediatype/mediatype.go rename to pkg/mediatype/mediatype.go diff --git a/lib/mediatype/mediatype_test.go b/pkg/mediatype/mediatype_test.go similarity index 97% rename from lib/mediatype/mediatype_test.go rename to pkg/mediatype/mediatype_test.go index 8d83c48..add0e21 100644 --- a/lib/mediatype/mediatype_test.go +++ b/pkg/mediatype/mediatype_test.go @@ -5,7 +5,7 @@ import ( "reflect" "testing" - "github.com/glass-cms/glasscms/lib/mediatype" + "github.com/glass-cms/glasscms/pkg/mediatype" ) func stringPtr(s string) *string { diff --git a/lib/middleware/accept.go b/pkg/middleware/accept.go similarity index 95% rename from lib/middleware/accept.go rename to pkg/middleware/accept.go index 43c2b8a..7d22913 100644 --- a/lib/middleware/accept.go +++ b/pkg/middleware/accept.go @@ -4,7 +4,7 @@ import ( "net/http" "slices" - "github.com/glass-cms/glasscms/lib/mediatype" + "github.com/glass-cms/glasscms/pkg/mediatype" ) // Accept generates a handler that writes a 415 Unsupported Media Type header diff --git a/lib/middleware/accept_test.go b/pkg/middleware/accept_test.go similarity index 97% rename from lib/middleware/accept_test.go rename to pkg/middleware/accept_test.go index 49799f8..70f6665 100644 --- a/lib/middleware/accept_test.go +++ b/pkg/middleware/accept_test.go @@ -5,7 +5,7 @@ import ( "net/http/httptest" "testing" - "github.com/glass-cms/glasscms/lib/middleware" + "github.com/glass-cms/glasscms/pkg/middleware" ) func Test_Accept(t *testing.T) { diff --git a/lib/middleware/content_type.go b/pkg/middleware/content_type.go similarity index 94% rename from lib/middleware/content_type.go rename to pkg/middleware/content_type.go index 327a394..d3c060a 100644 --- a/lib/middleware/content_type.go +++ b/pkg/middleware/content_type.go @@ -4,7 +4,7 @@ import ( "net/http" "slices" - "github.com/glass-cms/glasscms/lib/mediatype" + "github.com/glass-cms/glasscms/pkg/mediatype" ) // ContentType generates a handler that writes a 415 Unsupported Media Type header diff --git a/lib/middleware/content_type_test.go b/pkg/middleware/content_type_test.go similarity index 94% rename from lib/middleware/content_type_test.go rename to pkg/middleware/content_type_test.go index 20c60d1..92b9d2c 100644 --- a/lib/middleware/content_type_test.go +++ b/pkg/middleware/content_type_test.go @@ -5,8 +5,8 @@ import ( "net/http/httptest" "testing" - "github.com/glass-cms/glasscms/lib/mediatype" - "github.com/glass-cms/glasscms/lib/middleware" + "github.com/glass-cms/glasscms/pkg/mediatype" + "github.com/glass-cms/glasscms/pkg/middleware" ) func Test_ContentType(t *testing.T) { diff --git a/lib/resource/resource.go b/pkg/resource/resource.go similarity index 100% rename from lib/resource/resource.go rename to pkg/resource/resource.go diff --git a/lib/test/errorreadcloser.go b/pkg/test/errorreadcloser.go similarity index 100% rename from lib/test/errorreadcloser.go rename to pkg/test/errorreadcloser.go diff --git a/scripts/compile-spec.sh b/scripts/compile-spec.sh index 2e9b613..c8ca696 100755 --- a/scripts/compile-spec.sh +++ b/scripts/compile-spec.sh @@ -6,5 +6,5 @@ current_dir=$(pwd) cd typespec || exit -tsp compile . --emit @typespec/openapi3 --option "@typespec/openapi3.emitter-output-dir=${current_dir}/openapi" +tsp compile . --emit @typespec/openapi3 --option "@typespec/openapi3.emitter-output-dir=${current_dir}" cd "$current_dir" || exit diff --git a/server/handler/handler.go b/server/handler/handler.go deleted file mode 100644 index c581fae..0000000 --- a/server/handler/handler.go +++ /dev/null @@ -1,9 +0,0 @@ -package handler - -import "net/http" - -// VersionedHandler is an interface that defines an HTTP handler. -type VersionedHandler interface { - // HttpHandler returns an http.Handler that implements the API for a specific version. - Handler(baseRouter *http.ServeMux, middlewares []func(http.Handler) http.Handler) http.Handler -} diff --git a/server/handler/v1/handler_v1.go b/server/handler/v1/handler_v1.go deleted file mode 100644 index fb715b8..0000000 --- a/server/handler/v1/handler_v1.go +++ /dev/null @@ -1,67 +0,0 @@ -// Package v1 implements the API handlers for the v1 version of the Glass CMS API. -package v1 - -import ( - "log/slog" - "net/http" - "reflect" - - v1 "github.com/glass-cms/glasscms/api/v1" - "github.com/glass-cms/glasscms/item" - "github.com/glass-cms/glasscms/lib/resource" - "github.com/glass-cms/glasscms/server/handler" -) - -type APIHandler struct { - logger *slog.Logger - itemService *item.Service - - errorHandler *ErrorHandler -} - -// NewAPIHandler returns a new instance of ApiHandler. -func NewAPIHandler( - logger *slog.Logger, - service *item.Service, -) *APIHandler { - return &APIHandler{ - logger: logger, - itemService: service, - errorHandler: NewErrorHandler(), - } -} - -// Handler returns an http.Handler that implements the API. -func (s *APIHandler) Handler( - baseRouter *http.ServeMux, - middlewares []func(http.Handler) http.Handler, -) http.Handler { - convertedMiddlewares := make([]v1.MiddlewareFunc, len(middlewares)) - for i, mw := range middlewares { - convertedMiddlewares[i] = v1.MiddlewareFunc(mw) - } - - s.registerErrorMappers() - - return v1.HandlerWithOptions(s, v1.StdHTTPServerOptions{ - BaseURL: "/v1", - BaseRouter: baseRouter, - Middlewares: convertedMiddlewares, - ErrorHandlerFunc: s.errorHandler.HandleError, - }) -} - -func (s *APIHandler) registerErrorMappers() { - s.errorHandler.RegisterErrorMapper( - reflect.TypeOf(&resource.AlreadyExistsError{}), - ErrorMapperAlreadyExistsError, - ) - - s.errorHandler.RegisterErrorMapper( - reflect.TypeOf(&resource.NotFoundError{}), - ErrorMapperNotFoundError, - ) -} - -var _ v1.ServerInterface = (*APIHandler)(nil) -var _ handler.VersionedHandler = (*APIHandler)(nil) diff --git a/server/handler/v1/item.go b/server/handler/v1/item.go deleted file mode 100644 index ea04dd3..0000000 --- a/server/handler/v1/item.go +++ /dev/null @@ -1,53 +0,0 @@ -package v1 - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - - v1 "github.com/glass-cms/glasscms/api/v1" - "github.com/glass-cms/glasscms/server/handler" -) - -func (s *APIHandler) ItemsCreate(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - reqBody, err := io.ReadAll(r.Body) - if err != nil { - s.logger.ErrorContext(ctx, fmt.Errorf("failed to read request body: %w", err).Error()) - s.errorHandler.HandleError(w, r, err) - return - } - - var request *v1.ItemsCreateJSONRequestBody - if err = json.Unmarshal(reqBody, &request); err != nil { - s.logger.ErrorContext(ctx, fmt.Errorf("failed to unmarshal request body: %w", err).Error()) - s.errorHandler.HandleError(w, r, err) - return - } - - err = s.itemService.CreateItem(ctx, request.ToItem()) - if err != nil { - s.logger.ErrorContext(ctx, fmt.Errorf("failed to create item: %w", err).Error()) - s.errorHandler.HandleError(w, r, err) - return - } - - // TODO: Write response. - - w.WriteHeader(http.StatusCreated) -} - -func (s *APIHandler) ItemsGet(w http.ResponseWriter, r *http.Request, name v1.ItemKey) { - ctx := r.Context() - - item, err := s.itemService.GetItem(ctx, name) - if err != nil { - s.logger.ErrorContext(ctx, fmt.Errorf("failed to get item: %w", err).Error()) - s.errorHandler.HandleError(w, r, err) - return - } - - handler.SerializeResponse(w, r, http.StatusOK, item) -} diff --git a/server/server.go b/server/server.go deleted file mode 100644 index 4d00a49..0000000 --- a/server/server.go +++ /dev/null @@ -1,78 +0,0 @@ -package server - -import ( - "context" - "fmt" - "log/slog" - "net/http" - "time" - - "github.com/glass-cms/glasscms/lib/mediatype" - "github.com/glass-cms/glasscms/lib/middleware" - "github.com/glass-cms/glasscms/server/handler" -) - -const ( - ShutdownGracePeriod = 10 * time.Second - DefaultPort = 8080 - DefaultReadTimeout = 5 * time.Second - DefaultWriteTimeout = 10 * time.Second -) - -type Server struct { - logger *slog.Logger - server *http.Server -} - -func New( - logger *slog.Logger, - handlers []handler.VersionedHandler, - opts ...Option, -) (*Server, error) { - server := &Server{ - logger: logger, - } - - serveMux := http.NewServeMux() - - for _, h := range handlers { - _ = h.Handler(serveMux, []func(http.Handler) http.Handler{ - middleware.ContentType(mediatype.ApplicationJSON), - middleware.Accept(mediatype.ApplicationJSON), - }) - } - - server.server = &http.Server{ - Handler: serveMux, - Addr: fmt.Sprintf(":%v", DefaultPort), - ReadTimeout: DefaultReadTimeout, - WriteTimeout: DefaultWriteTimeout, - } - - for _, opt := range opts { - if err := opt(server); err != nil { - return nil, err - } - } - - return server, nil -} - -// ListenAndServe starts the server. -func (s *Server) ListenAndServer() error { - s.logger.Info("server is listening on :8080") - return s.server.ListenAndServe() -} - -// Shutdown gracefully shuts down the underlying server without interrupting any active connections. -func (s *Server) Shutdown() { - ctx, cancel := context.WithTimeout(context.Background(), ShutdownGracePeriod) - defer cancel() - - if err := s.server.Shutdown(ctx); err != nil { - s.logger.Error("could not gracefully shutdown the server:", "err", err) - return - } - - s.logger.Info("server stopped") -} diff --git a/typespec/main.tsp b/typespec/main.tsp index 7fbd8c0..4e71eff 100644 --- a/typespec/main.tsp +++ b/typespec/main.tsp @@ -1,7 +1,6 @@ import "@typespec/http"; import "@typespec/rest"; import "@typespec/openapi3"; -import "@typespec/versioning"; import "./error.tsp"; using TypeSpec.Http; @@ -10,7 +9,6 @@ using TypeSpec.Rest; @service({ title: "GlassCMS API", }) -@TypeSpec.Versioning.versioned(Versions) namespace GlassCMSCore; enum Versions {