diff --git a/Taskfile.yml b/Taskfile.yml index c22a1b2..4916f02 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -1,22 +1,22 @@ -version: '3' +version: "3" # Глобальные переменные проекта vars: - GO_VERSION: '1.25.6' - GOLANGCI_LINT_VERSION: 'v2.8.0' - GCI_VERSION: 'v0.13.7' - GOFUMPT_VERSION: 'v0.9.2' - BUF_VERSION: '1.64.0' - PROTOC_GEN_GO_VERSION: 'v1.36.11' - PROTOC_GEN_GO_GRPC_VERSION: 'v1.6.0' - - BIN_DIR: '{{.ROOT_DIR}}/bin' - GOLANGCI_LINT: '{{.BIN_DIR}}/golangci-lint' - GCI: '{{.BIN_DIR}}/gci' - GOFUMPT: '{{.BIN_DIR}}/gofumpt' - BUF: '{{.BIN_DIR}}/buf' - PROTOC_GEN_GO: '{{.BIN_DIR}}/protoc-gen-go' - PROTOC_GEN_GO_GRPC: '{{.BIN_DIR}}/protoc-gen-go-grpc' + GO_VERSION: "1.25.6" + GOLANGCI_LINT_VERSION: "v2.8.0" + GCI_VERSION: "v0.13.7" + GOFUMPT_VERSION: "v0.9.2" + BUF_VERSION: "1.64.0" + PROTOC_GEN_GO_VERSION: "v1.36.11" + PROTOC_GEN_GO_GRPC_VERSION: "v1.6.0" + + BIN_DIR: "{{.ROOT_DIR}}/bin" + GOLANGCI_LINT: "{{.BIN_DIR}}/golangci-lint" + GCI: "{{.BIN_DIR}}/gci" + GOFUMPT: "{{.BIN_DIR}}/gofumpt" + BUF: "{{.BIN_DIR}}/buf" + PROTOC_GEN_GO: "{{.BIN_DIR}}/protoc-gen-go" + PROTOC_GEN_GO_GRPC: "{{.BIN_DIR}}/protoc-gen-go-grpc" MODULES: inventory payment order shared @@ -39,20 +39,23 @@ tasks: format: desc: "Форматирует весь проект gofumpt + gci, исключая mocks" - deps: [ install-formatters ] + deps: [install-formatters] cmds: - | echo "🧼 Форматируем через gofumpt ..." - + for module in {{.MODULES}}; do if [ -d "$module" ]; then echo "🧼 Форматируем $module" - find $module -type f -name '*.go' ! -path '*/mocks/*' -exec {{.GOFUMPT}} -extra -w {} + + find "$module" -type f -name '*.go' \ + ! -path '*/mocks/*' \ + ! -path '*/shared/pkg/*' \ + -exec {{.GOFUMPT}} -extra -w {} + fi done - | echo "🎯 Сортируем импорты через gci ..." - + for module in {{.MODULES}}; do if [ -d "$module" ]; then echo "🎯 Сортируем импорты в $module" @@ -74,10 +77,10 @@ tasks: lint: desc: "Запускает golangci-lint для всех модулей" - deps: [ install-golangci-lint ] + deps: [install-golangci-lint, format] vars: - MODULES: '{{.MODULES}}' - GOLANGCI_LINT: '{{.GOLANGCI_LINT}}' + MODULES: "{{.MODULES}}" + GOLANGCI_LINT: "{{.GOLANGCI_LINT}}" cmds: - | set -e @@ -93,10 +96,10 @@ tasks: lint:fix: desc: "Запускает golangci-lint для всех модулей" - deps: [ install-golangci-lint ] + deps: [install-golangci-lint] vars: - MODULES: '{{.MODULES}}' - GOLANGCI_LINT: '{{.GOLANGCI_LINT}}' + MODULES: "{{.MODULES}}" + GOLANGCI_LINT: "{{.GOLANGCI_LINT}}" cmds: - | set -e @@ -150,7 +153,7 @@ tasks: } proto:update-deps: - deps: [ install-buf ] + deps: [install-buf] desc: Обновляет зависимости protobuf из удаленных репозиториев (googleapis и т.д.) dir: shared cmds: @@ -159,7 +162,7 @@ tasks: {{.BUF}} dep update proto:lint: - deps: [ proto:install-plugins, proto:update-deps ] + deps: [proto:install-plugins, proto:update-deps] desc: Проверка .proto-файлов на соответствие стилю dir: shared cmds: @@ -168,7 +171,7 @@ tasks: {{.BUF}} lint proto:gen: - deps: [ install-buf, proto:install-plugins, proto:update-deps, proto:lint ] + deps: [install-buf, proto:install-plugins, proto:update-deps, proto:lint] desc: Генерация Go-кода из .proto dir: shared cmds: @@ -183,4 +186,3 @@ tasks: - | echo "🏗️ Генерируем Go код из openapi..." go generate ./... - diff --git a/inventory/cmd/main.go b/inventory/cmd/main.go index 7a44657..b55ffb1 100644 --- a/inventory/cmd/main.go +++ b/inventory/cmd/main.go @@ -1,77 +1,23 @@ package main import ( - "context" "fmt" "log" "net" "os" "os/signal" - "slices" - "sync" "syscall" - "github.com/brianvoe/gofakeit/v7" "google.golang.org/grpc" - "google.golang.org/grpc/codes" "google.golang.org/grpc/reflection" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/types/known/timestamppb" - inventoryv1 "github.com/qyrlabs/test-backend/shared/pkg/proto/inventory/v1" + apiinventoryv1 "github.com/qyrlabs/test-backend/inventory/internal/api/inventory/v1" + partRepository "github.com/qyrlabs/test-backend/inventory/internal/repository/part" + partService "github.com/qyrlabs/test-backend/inventory/internal/service/part" + protoinventoryv1 "github.com/qyrlabs/test-backend/shared/pkg/proto/inventory/v1" ) -const grpcPort = 50061 - -type inventoryService struct { - inventoryv1.UnimplementedInventoryServiceServer - - mu sync.RWMutex - parts map[string]*inventoryv1.Part -} - -// Get part info by its UUID. -func (s *inventoryService) GetPart(ctx context.Context, req *inventoryv1.GetPartRequest) (*inventoryv1.GetPartResponse, error) { - s.mu.RLock() - defer s.mu.RUnlock() - part, ok := s.parts[req.GetUuid()] - if !ok { - return nil, status.Errorf(codes.NotFound, "part with uuid %s is not found", req.GetUuid()) - } - - return &inventoryv1.GetPartResponse{ - Part: part, - }, nil -} - -// Returns List of Parts by filter. -func (s *inventoryService) ListParts(ctx context.Context, req *inventoryv1.ListPartsRequest) (*inventoryv1.ListPartsResponse, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - filter := req.GetFilter() - uuids := filter.GetUuids() - names := filter.GetNames() - categories := filter.GetCategories() - countries := filter.GetManufacturerCountries() - tags := filter.GetTags() - - filteredParts := make([]*inventoryv1.Part, 0) - for uuid, part := range s.parts { - if (uuids == nil || slices.Contains(uuids, uuid)) && - (names == nil || slices.Contains(names, part.GetName())) && - (categories == nil || slices.Contains(categories, part.GetCategory())) && - (countries == nil || slices.Contains(countries, part.GetManufacturer().GetCountry())) && - (tags == nil || slices.Equal(tags, part.GetTags())) { - - filteredParts = append(filteredParts, part) - } - } - - return &inventoryv1.ListPartsResponse{ - Parts: filteredParts, - }, nil -} +const grpcPort = 50052 func main() { lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", grpcPort)) @@ -83,11 +29,11 @@ func main() { grpcServer := grpc.NewServer() reflection.Register(grpcServer) - service := &inventoryService{ - parts: initParts(100), - } + repo := partRepository.NewRepository() + service := partService.NewService(repo) + api := apiinventoryv1.NewAPI(service) - inventoryv1.RegisterInventoryServiceServer(grpcServer, service) + protoinventoryv1.RegisterInventoryServiceServer(grpcServer, api) go func() { log.Printf("gRPC server listening on %s\n", lis.Addr().String()) @@ -106,68 +52,3 @@ func main() { grpcServer.GracefulStop() log.Println("gRPC server stopped") } - -func initParts(count int) map[string]*inventoryv1.Part { - parts := createParts(count) - partsMap := make(map[string]*inventoryv1.Part, count) - for _, part := range parts { - partsMap[part.GetUuid()] = part - } - return partsMap -} - -func fakeDimensions() *inventoryv1.Dimensions { - return &inventoryv1.Dimensions{ - Length: gofakeit.Float64Range(1.0, 300.0), - Width: gofakeit.Float64Range(1.0, 300.0), - Height: gofakeit.Float64Range(0.5, 150.0), - Weight: gofakeit.Float64Range(0.1, 500.0), - } -} - -func fakeManufacturer() *inventoryv1.Manufacturer { - return &inventoryv1.Manufacturer{ - Name: gofakeit.Company(), - Country: gofakeit.Country(), - Website: gofakeit.URL(), - } -} - -func fakeTags() []string { - tags := make([]string, 0, 5) - for range gofakeit.IntRange(1, 5) { - tags = append(tags, gofakeit.Word()) - } - return tags -} - -func randomCategory() inventoryv1.Category { - // Ignore any, but not UNSPECIFIED - vals := []inventoryv1.Category{ - inventoryv1.Category_CATEGORY_ENGINE, - inventoryv1.Category_CATEGORY_FUEL, - inventoryv1.Category_CATEGORY_PORTHOLE, - inventoryv1.Category_CATEGORY_WING, - } - return vals[gofakeit.IntRange(0, len(vals)-1)] -} - -func createParts(count int) []*inventoryv1.Part { - parts := make([]*inventoryv1.Part, 0, count) - for range count { - parts = append(parts, &inventoryv1.Part{ - Uuid: gofakeit.UUID(), - Name: gofakeit.Name(), - Description: gofakeit.Sentence(10), - PriceMinor: int64(gofakeit.IntRange(1, 100000)), - StockQuantity: int64(gofakeit.IntRange(1, 100)), - Category: randomCategory(), - Dimensions: fakeDimensions(), - Manufacturer: fakeManufacturer(), - Tags: fakeTags(), - CreatedAt: timestamppb.New(gofakeit.Date()), - UpdatedAt: timestamppb.New(gofakeit.Date()), - }) - } - return parts -} diff --git a/inventory/go.mod b/inventory/go.mod index 09995c4..b465265 100644 --- a/inventory/go.mod +++ b/inventory/go.mod @@ -6,7 +6,9 @@ replace github.com/qyrlabs/test-backend/shared => ../shared require ( github.com/brianvoe/gofakeit/v7 v7.14.0 + github.com/google/uuid v1.6.0 github.com/qyrlabs/test-backend/shared v0.0.0-00010101000000-000000000000 + github.com/samber/lo v1.52.0 google.golang.org/grpc v1.78.0 google.golang.org/protobuf v1.36.11 ) diff --git a/inventory/go.sum b/inventory/go.sum index 2f79f3b..6d00149 100644 --- a/inventory/go.sum +++ b/inventory/go.sum @@ -10,6 +10,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= +github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= diff --git a/inventory/internal/api/inventory/v1/api.go b/inventory/internal/api/inventory/v1/api.go new file mode 100644 index 0000000..166da94 --- /dev/null +++ b/inventory/internal/api/inventory/v1/api.go @@ -0,0 +1,18 @@ +package v1 + +import ( + "github.com/qyrlabs/test-backend/inventory/internal/service" + inventoryv1 "github.com/qyrlabs/test-backend/shared/pkg/proto/inventory/v1" +) + +type api struct { + inventoryv1.UnimplementedInventoryServiceServer + + inventoryService service.PartService +} + +func NewAPI(inventoryService service.PartService) *api { + return &api{ + inventoryService: inventoryService, + } +} diff --git a/inventory/internal/api/inventory/v1/get.go b/inventory/internal/api/inventory/v1/get.go new file mode 100644 index 0000000..5388eb0 --- /dev/null +++ b/inventory/internal/api/inventory/v1/get.go @@ -0,0 +1,37 @@ +package v1 + +import ( + "context" + "errors" + "log" + + "github.com/google/uuid" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/qyrlabs/test-backend/inventory/internal/converter" + "github.com/qyrlabs/test-backend/inventory/internal/model" + inventoryv1 "github.com/qyrlabs/test-backend/shared/pkg/proto/inventory/v1" +) + +// Get part info by its UUID. +func (a *api) GetPart(ctx context.Context, req *inventoryv1.GetPartRequest) (*inventoryv1.GetPartResponse, error) { + partUUID := req.GetUuid() + + if _, err := uuid.Parse(partUUID); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid uuid format: %v", err) + } + + part, err := a.inventoryService.Get(ctx, req.GetUuid()) + if err != nil { + if errors.Is(err, model.ErrPartNotFound) { + return nil, status.Errorf(codes.NotFound, "part with uuid %s is not found", req.GetUuid()) + } + log.Printf("failed to get part with uuid %s: %v", req.GetUuid(), err) + return nil, status.Error(codes.Internal, "internal error") + } + + return &inventoryv1.GetPartResponse{ + Part: converter.ToProtoPart(part), + }, nil +} diff --git a/inventory/internal/api/inventory/v1/list.go b/inventory/internal/api/inventory/v1/list.go new file mode 100644 index 0000000..535f6d2 --- /dev/null +++ b/inventory/internal/api/inventory/v1/list.go @@ -0,0 +1,27 @@ +package v1 + +import ( + "context" + "log" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/qyrlabs/test-backend/inventory/internal/converter" + inventoryv1 "github.com/qyrlabs/test-backend/shared/pkg/proto/inventory/v1" +) + +// Returns List of Parts by filter. +func (a *api) ListParts(ctx context.Context, req *inventoryv1.ListPartsRequest) (*inventoryv1.ListPartsResponse, error) { + filteredParts, err := a.inventoryService.List(ctx, converter.ToProtoFilter(req.GetFilter())) + if err != nil { + log.Printf("failed to list parts: %v", err) + return nil, status.Error(codes.Internal, "internal error") + } + + protoParts := converter.ToProtoParts(filteredParts) + + return &inventoryv1.ListPartsResponse{ + Parts: protoParts, + }, nil +} diff --git a/inventory/internal/converter/part.go b/inventory/internal/converter/part.go new file mode 100644 index 0000000..2c7edcd --- /dev/null +++ b/inventory/internal/converter/part.go @@ -0,0 +1,156 @@ +package converter + +import ( + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/qyrlabs/test-backend/inventory/internal/model" + inventoryv1 "github.com/qyrlabs/test-backend/shared/pkg/proto/inventory/v1" +) + +func ToProtoPart(part *model.Part) *inventoryv1.Part { + return &inventoryv1.Part{ + Uuid: part.Uuid, + Name: part.Name, + Description: part.Description, + PriceMinor: part.PriceMinor, + StockQuantity: part.StockQuantity, + Category: ToProtoCategory(part.Category), + Dimensions: ToProtoDimensions(part.Dimensions), + Manufacturer: ToProtoManufacturer(part.Manufacturer), + Tags: part.Tags, + Metadata: ToProtoValueMap(part.Metadata), + CreatedAt: timestamppb.New(*part.CreatedAt), + UpdatedAt: timestamppb.New(*part.UpdatedAt), + } +} + +func ToProtoParts(parts []*model.Part) []*inventoryv1.Part { + protoParts := make([]*inventoryv1.Part, 0, len(parts)) + for _, part := range parts { + protoParts = append(protoParts, ToProtoPart(part)) + } + return protoParts +} + +func ToProtoCategory(category model.Category) inventoryv1.Category { + switch category { + case model.CategoryUnspecified: + return inventoryv1.Category_CATEGORY_UNSPECIFIED + case model.CategoryEngine: + return inventoryv1.Category_CATEGORY_ENGINE + case model.CategoryFuel: + return inventoryv1.Category_CATEGORY_FUEL + case model.CategoryPorthole: + return inventoryv1.Category_CATEGORY_PORTHOLE + case model.CategoryWing: + return inventoryv1.Category_CATEGORY_WING + default: + return inventoryv1.Category_CATEGORY_UNSPECIFIED + } +} + +func ToModelCategory(category inventoryv1.Category) model.Category { + switch category { + case inventoryv1.Category_CATEGORY_UNSPECIFIED: + return model.CategoryUnspecified + case inventoryv1.Category_CATEGORY_ENGINE: + return model.CategoryEngine + case inventoryv1.Category_CATEGORY_FUEL: + return model.CategoryFuel + case inventoryv1.Category_CATEGORY_PORTHOLE: + return model.CategoryPorthole + case inventoryv1.Category_CATEGORY_WING: + return model.CategoryWing + default: + return model.CategoryUnspecified + } +} + +func ToProtoDimensions(dimensions *model.Dimensions) *inventoryv1.Dimensions { + if dimensions == nil { + return nil + } + + return &inventoryv1.Dimensions{ + Length: dimensions.Length, + Width: dimensions.Width, + Height: dimensions.Height, + Weight: dimensions.Weight, + } +} + +func ToProtoManufacturer(manufacturer *model.Manufacturer) *inventoryv1.Manufacturer { + if manufacturer == nil { + return nil + } + + return &inventoryv1.Manufacturer{ + Name: manufacturer.Name, + Country: manufacturer.Country, + Website: manufacturer.Website, + } +} + +func ToProtoValueMap(metadata map[string]*model.Value) map[string]*inventoryv1.Value { + if metadata == nil { + return nil + } + + res := make(map[string]*inventoryv1.Value, len(metadata)) + for key, value := range metadata { + res[key] = ToProtoValue(value) + } + + return res +} + +func ToProtoValue(value *model.Value) *inventoryv1.Value { + if value == nil { + return nil + } + + protoValue := &inventoryv1.Value{} + + switch { + case value.StringValue != nil: + protoValue.Kind = &inventoryv1.Value_StringValue{StringValue: *value.StringValue} + case value.Int64Value != nil: + protoValue.Kind = &inventoryv1.Value_Int64Value{Int64Value: *value.Int64Value} + case value.DoubleValue != nil: + protoValue.Kind = &inventoryv1.Value_DoubleValue{DoubleValue: *value.DoubleValue} + case value.BoolValue != nil: + protoValue.Kind = &inventoryv1.Value_BoolValue{BoolValue: *value.BoolValue} + default: + return nil + } + + return protoValue +} + +func ToProtoFilter(filter *inventoryv1.PartsFilter) model.PartsFilter { + if filter == nil { + return model.PartsFilter{} + } + + categories := make([]model.Category, 0, len(filter.GetCategories())) + for _, cat := range filter.GetCategories() { + categories = append(categories, ToModelCategory(cat)) + } + + return model.PartsFilter{ + Uuids: copyPartsFilterField(filter.GetUuids()), + Names: copyPartsFilterField(filter.GetNames()), + Categories: categories, + ManufacturerCountries: copyPartsFilterField(filter.GetManufacturerCountries()), + Tags: copyPartsFilterField(filter.GetTags()), + } +} + +func copyPartsFilterField(v []string) []string { + if len(v) == 0 { + return nil + } + res := make([]string, len(v)) + copy(res, v) + return res +} diff --git a/inventory/internal/model/errors.go b/inventory/internal/model/errors.go new file mode 100644 index 0000000..23ade3a --- /dev/null +++ b/inventory/internal/model/errors.go @@ -0,0 +1,5 @@ +package model + +import "errors" + +var ErrPartNotFound = errors.New("part not found") diff --git a/inventory/internal/model/part.go b/inventory/internal/model/part.go new file mode 100644 index 0000000..5f13ad8 --- /dev/null +++ b/inventory/internal/model/part.go @@ -0,0 +1,74 @@ +package model + +import ( + "time" +) + +type Part struct { + // Unique identifier of the part. + Uuid string + // Name of the part. + Name string + // Description of the part. + Description string + // Unit price. + PriceMinor int64 + // Quantity available in stock. + StockQuantity int64 + // Part category. + Category Category + // Part dimensions. + Dimensions *Dimensions + // Manufacturer information. + Manufacturer *Manufacturer + // Tags for quick search. + Tags []string + // Flexible metadata. + Metadata map[string]*Value + // Creation timestamp. + CreatedAt *time.Time + // Last update timestamp. + UpdatedAt *time.Time +} + +// Category of the Part. +type Category int32 + +const ( + CategoryUnspecified Category = 0 + CategoryEngine Category = 1 + CategoryFuel Category = 2 + CategoryPorthole Category = 3 + CategoryWing Category = 4 +) + +// Dimenstions of the Part. +type Dimensions struct { + Length float64 + Width float64 + Height float64 + Weight float64 +} + +// Manufacturer of the Part. +type Manufacturer struct { + Name string + Country string + Website string +} + +// Value represents a typed metadata value. +type Value struct { + StringValue *string + Int64Value *int64 + DoubleValue *float64 + BoolValue *bool +} + +type PartsFilter struct { + Uuids []string + Names []string + Categories []Category + ManufacturerCountries []string + Tags []string +} diff --git a/inventory/internal/repository/converter/part.go b/inventory/internal/repository/converter/part.go new file mode 100644 index 0000000..2557420 --- /dev/null +++ b/inventory/internal/repository/converter/part.go @@ -0,0 +1,86 @@ +package converter + +import ( + "github.com/qyrlabs/test-backend/inventory/internal/model" + "github.com/qyrlabs/test-backend/inventory/internal/repository/repomodel" +) + +func ToModelPart(part repomodel.Part) *model.Part { + return &model.Part{ + Uuid: part.Uuid, + Name: part.Name, + Description: part.Description, + PriceMinor: part.PriceMinor, + StockQuantity: part.StockQuantity, + Category: ToModelCategory(part.Category), + Dimensions: ToModelDimensions(part.Dimensions), + Manufacturer: ToModelManufacturer(part.Manufacturer), + Tags: part.Tags, + Metadata: ToModelValueMap(part.Metadata), + CreatedAt: part.CreatedAt, + UpdatedAt: part.UpdatedAt, + } +} + +func ToModelCategory(category repomodel.Category) model.Category { + switch category { + case repomodel.CategoryUnspecified: + return model.CategoryUnspecified + case repomodel.CategoryEngine: + return model.CategoryEngine + case repomodel.CategoryFuel: + return model.CategoryFuel + case repomodel.CategoryPorthole: + return model.CategoryPorthole + case repomodel.CategoryWing: + return model.CategoryWing + default: + return model.CategoryUnspecified + } +} + +func ToModelDimensions(dimensions *repomodel.Dimensions) *model.Dimensions { + if dimensions == nil { + return nil + } + return &model.Dimensions{ + Length: dimensions.Length, + Width: dimensions.Width, + Height: dimensions.Height, + Weight: dimensions.Weight, + } +} + +func ToModelManufacturer(manufacturer *repomodel.Manufacturer) *model.Manufacturer { + if manufacturer == nil { + return nil + } + return &model.Manufacturer{ + Name: manufacturer.Name, + Country: manufacturer.Country, + Website: manufacturer.Website, + } +} + +func ToModelValueMap(metadata map[string]*repomodel.Value) map[string]*model.Value { + if metadata == nil { + return nil + } + result := make(map[string]*model.Value, len(metadata)) + for key, value := range metadata { + result[key] = ToModelValue(value) + } + return result +} + +func ToModelValue(value *repomodel.Value) *model.Value { + if value == nil { + return nil + } + return &model.Value{ + StringValue: value.StringValue, + Int64Value: value.Int64Value, + DoubleValue: value.DoubleValue, + BoolValue: value.BoolValue, + } +} diff --git a/inventory/internal/repository/part/get.go b/inventory/internal/repository/part/get.go new file mode 100644 index 0000000..4d3591f --- /dev/null +++ b/inventory/internal/repository/part/get.go @@ -0,0 +1,19 @@ +package part + +import ( + "context" + + "github.com/qyrlabs/test-backend/inventory/internal/model" + "github.com/qyrlabs/test-backend/inventory/internal/repository/converter" +) + +func (r *repository) Get(ctx context.Context, uuid string) (*model.Part, error) { + r.mu.RLock() + defer r.mu.RUnlock() + part, ok := r.parts[uuid] + if !ok { + return nil, model.ErrPartNotFound + } + + return converter.ToModelPart(part), nil +} diff --git a/inventory/internal/repository/part/init.go b/inventory/internal/repository/part/init.go new file mode 100644 index 0000000..4c2ee3a --- /dev/null +++ b/inventory/internal/repository/part/init.go @@ -0,0 +1,78 @@ +package part + +import ( + "fmt" + + "github.com/brianvoe/gofakeit/v7" + "github.com/samber/lo" + + "github.com/qyrlabs/test-backend/inventory/internal/repository/repomodel" +) + +func (r *repository) initParts(count int) error { + if count < 0 { + return fmt.Errorf("failed to execute initParts") + } + + parts := createParts(count) + for _, part := range parts { + r.parts[part.Uuid] = part + } + return nil +} + +func fakeDimensions() *repomodel.Dimensions { + return &repomodel.Dimensions{ + Length: gofakeit.Float64Range(1.0, 300.0), + Width: gofakeit.Float64Range(1.0, 300.0), + Height: gofakeit.Float64Range(0.5, 150.0), + Weight: gofakeit.Float64Range(0.1, 500.0), + } +} + +func fakeManufacturer() *repomodel.Manufacturer { + return &repomodel.Manufacturer{ + Name: gofakeit.Company(), + Country: gofakeit.Country(), + Website: gofakeit.URL(), + } +} + +func fakeTags() []string { + tags := make([]string, 0, 5) + for range gofakeit.IntRange(1, 5) { + tags = append(tags, gofakeit.Word()) + } + return tags +} + +func randomCategory() repomodel.Category { + // Ignore any, but not UNSPECIFIED + vals := []repomodel.Category{ + repomodel.CategoryEngine, + repomodel.CategoryFuel, + repomodel.CategoryPorthole, + repomodel.CategoryWing, + } + return vals[gofakeit.IntRange(0, len(vals)-1)] +} + +func createParts(count int) []repomodel.Part { + parts := make([]repomodel.Part, 0, count) + for range count { + parts = append(parts, repomodel.Part{ + Uuid: gofakeit.UUID(), + Name: gofakeit.Name(), + Description: gofakeit.Sentence(10), + PriceMinor: int64(gofakeit.IntRange(1, 100000)), + StockQuantity: int64(gofakeit.IntRange(1, 100)), + Category: randomCategory(), + Dimensions: fakeDimensions(), + Manufacturer: fakeManufacturer(), + Tags: fakeTags(), + CreatedAt: lo.ToPtr(gofakeit.Date()), + UpdatedAt: lo.ToPtr(gofakeit.Date()), + }) + } + return parts +} diff --git a/inventory/internal/repository/part/list.go b/inventory/internal/repository/part/list.go new file mode 100644 index 0000000..63110bc --- /dev/null +++ b/inventory/internal/repository/part/list.go @@ -0,0 +1,35 @@ +package part + +import ( + "context" + "slices" + + "github.com/qyrlabs/test-backend/inventory/internal/model" + "github.com/qyrlabs/test-backend/inventory/internal/repository/converter" +) + +// Returns List of Parts by filter. +func (r *repository) List(ctx context.Context, filter model.PartsFilter) ([]*model.Part, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + uuids := filter.Uuids + names := filter.Names + categories := filter.Categories + countries := filter.ManufacturerCountries + tags := filter.Tags + + filteredParts := make([]*model.Part, 0) + for uuid, part := range r.parts { + if (len(uuids) == 0 || slices.Contains(uuids, uuid)) && + (len(names) == 0 || slices.Contains(names, part.Name)) && + (len(categories) == 0 || slices.Contains(categories, converter.ToModelCategory(part.Category))) && + (len(countries) == 0 || slices.Contains(countries, part.Manufacturer.Country)) && + (len(tags) == 0 || slices.Equal(tags, part.Tags)) { + partModel := converter.ToModelPart(part) + filteredParts = append(filteredParts, partModel) + } + } + + return filteredParts, nil +} diff --git a/inventory/internal/repository/part/repository.go b/inventory/internal/repository/part/repository.go new file mode 100644 index 0000000..d220fe4 --- /dev/null +++ b/inventory/internal/repository/part/repository.go @@ -0,0 +1,27 @@ +package part + +import ( + "log" + "sync" + + def "github.com/qyrlabs/test-backend/inventory/internal/repository" + "github.com/qyrlabs/test-backend/inventory/internal/repository/repomodel" +) + +var _ def.PartRepository = &repository{} + +type repository struct { + mu sync.RWMutex + parts map[string]repomodel.Part +} + +func NewRepository() *repository { + repository := repository{ + parts: make(map[string]repomodel.Part), + } + err := repository.initParts(100) + if err != nil { + log.Println("failed to init parts") + } + return &repository +} diff --git a/inventory/internal/repository/repomodel/part.go b/inventory/internal/repository/repomodel/part.go new file mode 100644 index 0000000..dd22125 --- /dev/null +++ b/inventory/internal/repository/repomodel/part.go @@ -0,0 +1,72 @@ +package repomodel + +import "time" + +type Part struct { + // Unique identifier of the part. + Uuid string + // Name of the part. + Name string + // Description of the part. + Description string + // Unit price. + PriceMinor int64 + // Quantity available in stock. + StockQuantity int64 + // Part category. + Category Category + // Part dimensions. + Dimensions *Dimensions + // Manufacturer information. + Manufacturer *Manufacturer + // Tags for quick search. + Tags []string + // Flexible metadata. + Metadata map[string]*Value + // Creation timestamp. + CreatedAt *time.Time + // Last update timestamp. + UpdatedAt *time.Time +} + +// Category of the Part. +type Category int32 + +const ( + CategoryUnspecified Category = 0 + CategoryEngine Category = 1 + CategoryFuel Category = 2 + CategoryPorthole Category = 3 + CategoryWing Category = 4 +) + +// Dimenstions of the Part. +type Dimensions struct { + Length float64 + Width float64 + Height float64 + Weight float64 +} + +// Manufacturer of the Part. +type Manufacturer struct { + Name string + Country string + Website string +} + +// Value represents a typed metadata value. +type Value struct { + StringValue *string + Int64Value *int64 + DoubleValue *float64 + BoolValue *bool +} + +type PartsFilter struct { + Uuids []string + Names []string + Categories []Category + ManufacturerCountries []string + Tags []string +} diff --git a/inventory/internal/repository/repository.go b/inventory/internal/repository/repository.go new file mode 100644 index 0000000..865f8df --- /dev/null +++ b/inventory/internal/repository/repository.go @@ -0,0 +1,12 @@ +package repository + +import ( + "context" + + "github.com/qyrlabs/test-backend/inventory/internal/model" +) + +type PartRepository interface { + Get(ctx context.Context, uuid string) (*model.Part, error) + List(ctx context.Context, filter model.PartsFilter) ([]*model.Part, error) +} diff --git a/inventory/internal/service/part/get.go b/inventory/internal/service/part/get.go new file mode 100644 index 0000000..f305fb4 --- /dev/null +++ b/inventory/internal/service/part/get.go @@ -0,0 +1,16 @@ +package part + +import ( + "context" + + "github.com/qyrlabs/test-backend/inventory/internal/model" +) + +// Get part info by its UUID. +func (s *service) Get(ctx context.Context, uuid string) (*model.Part, error) { + part, err := s.partRepository.Get(ctx, uuid) + if err != nil { + return nil, err + } + return part, nil +} diff --git a/inventory/internal/service/part/list.go b/inventory/internal/service/part/list.go new file mode 100644 index 0000000..e816b5a --- /dev/null +++ b/inventory/internal/service/part/list.go @@ -0,0 +1,16 @@ +package part + +import ( + "context" + + "github.com/qyrlabs/test-backend/inventory/internal/model" +) + +// Returns List of Parts by filter. +func (s *service) List(ctx context.Context, filter model.PartsFilter) ([]*model.Part, error) { + parts, err := s.partRepository.List(ctx, filter) + if err != nil { + return nil, err + } + return parts, nil +} diff --git a/inventory/internal/service/part/service.go b/inventory/internal/service/part/service.go new file mode 100644 index 0000000..cd7436a --- /dev/null +++ b/inventory/internal/service/part/service.go @@ -0,0 +1,18 @@ +package part + +import ( + "github.com/qyrlabs/test-backend/inventory/internal/repository" + def "github.com/qyrlabs/test-backend/inventory/internal/service" +) + +var _ def.PartService = &service{} + +type service struct { + partRepository repository.PartRepository +} + +func NewService(partRepository repository.PartRepository) *service { + return &service{ + partRepository: partRepository, + } +} diff --git a/inventory/internal/service/service.go b/inventory/internal/service/service.go new file mode 100644 index 0000000..3a42170 --- /dev/null +++ b/inventory/internal/service/service.go @@ -0,0 +1,12 @@ +package service + +import ( + "context" + + "github.com/qyrlabs/test-backend/inventory/internal/model" +) + +type PartService interface { + Get(ctx context.Context, uuid string) (*model.Part, error) + List(ctx context.Context, filter model.PartsFilter) ([]*model.Part, error) +} diff --git a/shared/api/order/v1/generate.go b/shared/api/order/v1/generate.go index 8789431..953019b 100644 --- a/shared/api/order/v1/generate.go +++ b/shared/api/order/v1/generate.go @@ -1,3 +1,3 @@ -package weather +package v1 -//go:generate go tool ogen --config ../../../ogen-config.yaml --target ../../../pkg/openapi/order/v1 --package orderv1 --clean order.v1.openapi.yaml +//go:generate go tool ogen --config ../../../ogen-config.yaml --target ../../../pkg/openapi/order/v1 --package v1 --clean order.v1.openapi.yaml diff --git a/shared/pkg/openapi/order/v1/oas_cfg_gen.go b/shared/pkg/openapi/order/v1/oas_cfg_gen.go index 363b665..6035b9d 100644 --- a/shared/pkg/openapi/order/v1/oas_cfg_gen.go +++ b/shared/pkg/openapi/order/v1/oas_cfg_gen.go @@ -1,6 +1,6 @@ // Code generated by ogen, DO NOT EDIT. -package orderv1 +package v1 import ( "net/http" diff --git a/shared/pkg/openapi/order/v1/oas_client_gen.go b/shared/pkg/openapi/order/v1/oas_client_gen.go index 60b51ac..4851b0c 100644 --- a/shared/pkg/openapi/order/v1/oas_client_gen.go +++ b/shared/pkg/openapi/order/v1/oas_client_gen.go @@ -1,6 +1,6 @@ // Code generated by ogen, DO NOT EDIT. -package orderv1 +package v1 import ( "context" diff --git a/shared/pkg/openapi/order/v1/oas_handlers_gen.go b/shared/pkg/openapi/order/v1/oas_handlers_gen.go index 366b87d..509220c 100644 --- a/shared/pkg/openapi/order/v1/oas_handlers_gen.go +++ b/shared/pkg/openapi/order/v1/oas_handlers_gen.go @@ -1,6 +1,6 @@ // Code generated by ogen, DO NOT EDIT. -package orderv1 +package v1 import ( "context" diff --git a/shared/pkg/openapi/order/v1/oas_interfaces_gen.go b/shared/pkg/openapi/order/v1/oas_interfaces_gen.go index 5c384a0..40dc7f3 100644 --- a/shared/pkg/openapi/order/v1/oas_interfaces_gen.go +++ b/shared/pkg/openapi/order/v1/oas_interfaces_gen.go @@ -1,5 +1,5 @@ // Code generated by ogen, DO NOT EDIT. -package orderv1 +package v1 type CancelOrderRes interface { cancelOrderRes() diff --git a/shared/pkg/openapi/order/v1/oas_json_gen.go b/shared/pkg/openapi/order/v1/oas_json_gen.go index 838758f..c45823a 100644 --- a/shared/pkg/openapi/order/v1/oas_json_gen.go +++ b/shared/pkg/openapi/order/v1/oas_json_gen.go @@ -1,6 +1,6 @@ // Code generated by ogen, DO NOT EDIT. -package orderv1 +package v1 import ( "math/bits" diff --git a/shared/pkg/openapi/order/v1/oas_labeler_gen.go b/shared/pkg/openapi/order/v1/oas_labeler_gen.go index 2f92fa5..9ac43f1 100644 --- a/shared/pkg/openapi/order/v1/oas_labeler_gen.go +++ b/shared/pkg/openapi/order/v1/oas_labeler_gen.go @@ -1,6 +1,6 @@ // Code generated by ogen, DO NOT EDIT. -package orderv1 +package v1 import ( "context" diff --git a/shared/pkg/openapi/order/v1/oas_middleware_gen.go b/shared/pkg/openapi/order/v1/oas_middleware_gen.go index e16eb46..46b506d 100644 --- a/shared/pkg/openapi/order/v1/oas_middleware_gen.go +++ b/shared/pkg/openapi/order/v1/oas_middleware_gen.go @@ -1,6 +1,6 @@ // Code generated by ogen, DO NOT EDIT. -package orderv1 +package v1 import ( "github.com/ogen-go/ogen/middleware" diff --git a/shared/pkg/openapi/order/v1/oas_operations_gen.go b/shared/pkg/openapi/order/v1/oas_operations_gen.go index 13af493..742429b 100644 --- a/shared/pkg/openapi/order/v1/oas_operations_gen.go +++ b/shared/pkg/openapi/order/v1/oas_operations_gen.go @@ -1,6 +1,6 @@ // Code generated by ogen, DO NOT EDIT. -package orderv1 +package v1 // OperationName is the ogen operation name type OperationName = string diff --git a/shared/pkg/openapi/order/v1/oas_parameters_gen.go b/shared/pkg/openapi/order/v1/oas_parameters_gen.go index 97fc8ab..829c442 100644 --- a/shared/pkg/openapi/order/v1/oas_parameters_gen.go +++ b/shared/pkg/openapi/order/v1/oas_parameters_gen.go @@ -1,6 +1,6 @@ // Code generated by ogen, DO NOT EDIT. -package orderv1 +package v1 import ( "net/http" diff --git a/shared/pkg/openapi/order/v1/oas_request_decoders_gen.go b/shared/pkg/openapi/order/v1/oas_request_decoders_gen.go index f82f78e..ce1fdd2 100644 --- a/shared/pkg/openapi/order/v1/oas_request_decoders_gen.go +++ b/shared/pkg/openapi/order/v1/oas_request_decoders_gen.go @@ -1,6 +1,6 @@ // Code generated by ogen, DO NOT EDIT. -package orderv1 +package v1 import ( "bytes" diff --git a/shared/pkg/openapi/order/v1/oas_request_encoders_gen.go b/shared/pkg/openapi/order/v1/oas_request_encoders_gen.go index 3b29ba2..1fed134 100644 --- a/shared/pkg/openapi/order/v1/oas_request_encoders_gen.go +++ b/shared/pkg/openapi/order/v1/oas_request_encoders_gen.go @@ -1,6 +1,6 @@ // Code generated by ogen, DO NOT EDIT. -package orderv1 +package v1 import ( "bytes" diff --git a/shared/pkg/openapi/order/v1/oas_response_decoders_gen.go b/shared/pkg/openapi/order/v1/oas_response_decoders_gen.go index 7b45a5d..2242ddf 100644 --- a/shared/pkg/openapi/order/v1/oas_response_decoders_gen.go +++ b/shared/pkg/openapi/order/v1/oas_response_decoders_gen.go @@ -1,6 +1,6 @@ // Code generated by ogen, DO NOT EDIT. -package orderv1 +package v1 import ( "io" diff --git a/shared/pkg/openapi/order/v1/oas_response_encoders_gen.go b/shared/pkg/openapi/order/v1/oas_response_encoders_gen.go index e3d7bb9..41cf6e4 100644 --- a/shared/pkg/openapi/order/v1/oas_response_encoders_gen.go +++ b/shared/pkg/openapi/order/v1/oas_response_encoders_gen.go @@ -1,6 +1,6 @@ // Code generated by ogen, DO NOT EDIT. -package orderv1 +package v1 import ( "net/http" diff --git a/shared/pkg/openapi/order/v1/oas_router_gen.go b/shared/pkg/openapi/order/v1/oas_router_gen.go index 8fddd48..23adc73 100644 --- a/shared/pkg/openapi/order/v1/oas_router_gen.go +++ b/shared/pkg/openapi/order/v1/oas_router_gen.go @@ -1,6 +1,6 @@ // Code generated by ogen, DO NOT EDIT. -package orderv1 +package v1 import ( "net/http" diff --git a/shared/pkg/openapi/order/v1/oas_schemas_gen.go b/shared/pkg/openapi/order/v1/oas_schemas_gen.go index 605071e..78999f7 100644 --- a/shared/pkg/openapi/order/v1/oas_schemas_gen.go +++ b/shared/pkg/openapi/order/v1/oas_schemas_gen.go @@ -1,6 +1,6 @@ // Code generated by ogen, DO NOT EDIT. -package orderv1 +package v1 import ( "fmt" diff --git a/shared/pkg/openapi/order/v1/oas_server_gen.go b/shared/pkg/openapi/order/v1/oas_server_gen.go index ffc88e9..926c921 100644 --- a/shared/pkg/openapi/order/v1/oas_server_gen.go +++ b/shared/pkg/openapi/order/v1/oas_server_gen.go @@ -1,6 +1,6 @@ // Code generated by ogen, DO NOT EDIT. -package orderv1 +package v1 import ( "context" diff --git a/shared/pkg/openapi/order/v1/oas_unimplemented_gen.go b/shared/pkg/openapi/order/v1/oas_unimplemented_gen.go index 9ebefb7..78adf26 100644 --- a/shared/pkg/openapi/order/v1/oas_unimplemented_gen.go +++ b/shared/pkg/openapi/order/v1/oas_unimplemented_gen.go @@ -1,6 +1,6 @@ // Code generated by ogen, DO NOT EDIT. -package orderv1 +package v1 import ( "context" diff --git a/shared/pkg/openapi/order/v1/oas_validators_gen.go b/shared/pkg/openapi/order/v1/oas_validators_gen.go index 1721994..a937503 100644 --- a/shared/pkg/openapi/order/v1/oas_validators_gen.go +++ b/shared/pkg/openapi/order/v1/oas_validators_gen.go @@ -1,6 +1,6 @@ // Code generated by ogen, DO NOT EDIT. -package orderv1 +package v1 import ( "github.com/go-faster/errors" diff --git a/shared/pkg/proto/inventory/v1/inventory.pb.go b/shared/pkg/proto/inventory/v1/inventory.pb.go index f7564d7..bebe0c4 100644 --- a/shared/pkg/proto/inventory/v1/inventory.pb.go +++ b/shared/pkg/proto/inventory/v1/inventory.pb.go @@ -4,7 +4,7 @@ // protoc (unknown) // source: inventory/v1/inventory.proto -package inventoryv1 +package v1 import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" @@ -795,7 +795,7 @@ const file_inventory_v1_inventory_proto_rawDesc = "" + "\rCATEGORY_WING\x10\x042\xa8\x01\n" + "\x10InventoryService\x12F\n" + "\aGetPart\x12\x1c.inventory.v1.GetPartRequest\x1a\x1d.inventory.v1.GetPartResponse\x12L\n" + - "\tListParts\x12\x1e.inventory.v1.ListPartsRequest\x1a\x1f.inventory.v1.ListPartsResponseBKZIgithub.com/qyrlabs/test-backend/shared/pkg/proto/inventory/v1;inventoryv1b\x06proto3" + "\tListParts\x12\x1e.inventory.v1.ListPartsRequest\x1a\x1f.inventory.v1.ListPartsResponseB?Z=github.com/qyrlabs/test-backend/shared/pkg/proto/inventory/v1b\x06proto3" var ( file_inventory_v1_inventory_proto_rawDescOnce sync.Once diff --git a/shared/pkg/proto/inventory/v1/inventory_grpc.pb.go b/shared/pkg/proto/inventory/v1/inventory_grpc.pb.go index c6a8c63..6b23c44 100644 --- a/shared/pkg/proto/inventory/v1/inventory_grpc.pb.go +++ b/shared/pkg/proto/inventory/v1/inventory_grpc.pb.go @@ -4,7 +4,7 @@ // - protoc (unknown) // source: inventory/v1/inventory.proto -package inventoryv1 +package v1 import ( context "context" diff --git a/shared/pkg/proto/payment/v1/payment.pb.go b/shared/pkg/proto/payment/v1/payment.pb.go index 31ce8b3..a0592be 100644 --- a/shared/pkg/proto/payment/v1/payment.pb.go +++ b/shared/pkg/proto/payment/v1/payment.pb.go @@ -4,7 +4,7 @@ // protoc (unknown) // source: payment/v1/payment.proto -package paymentv1 +package v1 import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" @@ -212,7 +212,7 @@ const file_payment_v1_payment_proto_rawDesc = "" + "\x1aPAYMENT_METHOD_CREDIT_CARD\x10\x03\x12!\n" + "\x1dPAYMENT_METHOD_INVESTOR_MONEY\x10\x042W\n" + "\x0ePaymentService\x12E\n" + - "\bPayOrder\x12\x1b.payment.v1.PayOrderRequest\x1a\x1c.payment.v1.PayOrderResponseBGZEgithub.com/qyrlabs/test-backend/shared/pkg/proto/payment/v1;paymentv1b\x06proto3" + "\bPayOrder\x12\x1b.payment.v1.PayOrderRequest\x1a\x1c.payment.v1.PayOrderResponseB=Z;github.com/qyrlabs/test-backend/shared/pkg/proto/payment/v1b\x06proto3" var ( file_payment_v1_payment_proto_rawDescOnce sync.Once diff --git a/shared/pkg/proto/payment/v1/payment_grpc.pb.go b/shared/pkg/proto/payment/v1/payment_grpc.pb.go index b25fcec..7334505 100644 --- a/shared/pkg/proto/payment/v1/payment_grpc.pb.go +++ b/shared/pkg/proto/payment/v1/payment_grpc.pb.go @@ -4,7 +4,7 @@ // - protoc (unknown) // source: payment/v1/payment.proto -package paymentv1 +package v1 import ( context "context" diff --git a/shared/proto/inventory/v1/inventory.proto b/shared/proto/inventory/v1/inventory.proto index 48d7e9c..0d8a0a8 100644 --- a/shared/proto/inventory/v1/inventory.proto +++ b/shared/proto/inventory/v1/inventory.proto @@ -4,7 +4,7 @@ package inventory.v1; import "google/protobuf/timestamp.proto"; -option go_package = "github.com/qyrlabs/test-backend/shared/pkg/proto/inventory/v1;inventoryv1"; +option go_package = "github.com/qyrlabs/test-backend/shared/pkg/proto/inventory/v1"; // Inventory Service stores and provides info about Parts(details). service InventoryService { diff --git a/shared/proto/payment/v1/payment.proto b/shared/proto/payment/v1/payment.proto index c9615e1..d6cfb1b 100644 --- a/shared/proto/payment/v1/payment.proto +++ b/shared/proto/payment/v1/payment.proto @@ -2,7 +2,7 @@ syntax = "proto3"; package payment.v1; -option go_package = "github.com/qyrlabs/test-backend/shared/pkg/proto/payment/v1;paymentv1"; +option go_package = "github.com/qyrlabs/test-backend/shared/pkg/proto/payment/v1"; // PaymentService provides operations for working with payments. service PaymentService {