diff --git a/design/design.go b/design/design.go index 9b9a6de..99f0121 100644 --- a/design/design.go +++ b/design/design.go @@ -19,4 +19,9 @@ var Character = Type("character", func() { Field(3, "description", String) Field(4, "class", String) Required("name", "description", "class") -}) \ No newline at end of file +}) + +var InventoryRecord = Type("inventoryRecord", func() { + Field(1, "characterId", Int) + Field(2, "itemId", Int) +}) diff --git a/services/character/character.go b/services/character/character.go index 1163a74..744c6b9 100644 --- a/services/character/character.go +++ b/services/character/character.go @@ -3,43 +3,82 @@ package characterapi import ( "context" character "crossnokaye-interview-assignment/services/character/gen/character" + "errors" "log" ) // character service example implementation. // The example methods log the requests and return zero values. type charactersrvc struct { - logger *log.Logger + logger *log.Logger + characters map[int]*character.Character } // NewCharacter returns the character service implementation. func NewCharacter(logger *log.Logger) character.Service { - return &charactersrvc{logger} + characterMap := make(map[int]*character.Character) + return &charactersrvc{logger, characterMap} } // GetCharacter implements getCharacter. -func (s *charactersrvc) GetCharacter(ctx context.Context, p int) (res *character.Character, err error) { - res = &character.Character{} +func (s *charactersrvc) GetCharacter(ctx context.Context, p *character.GetCharacterPayload) (res *character.Character, err error) { s.logger.Print("character.getCharacter") + + itemToGet := s.characters[*p.ID] + if itemToGet == nil { + s.logger.Printf("character with id %d not found", &p.ID) + return nil, errors.New("character not found") + } + res = s.characters[*p.ID] + return } // CreateCharacter implements createCharacter. func (s *charactersrvc) CreateCharacter(ctx context.Context, p *character.Character) (res *character.Character, err error) { - res = &character.Character{} s.logger.Print("character.createCharacter") + id := -1 + + // Using this method of assigning IDs means they will + // potentially be non-sequential if ids are deleted + if p.ID != nil { + id = *p.ID + } else { + id = len(s.characters) + } + + p.ID = &id + s.characters[id] = p + res = s.characters[id] return } // UpdateCharacter implements updateCharacter. func (s *charactersrvc) UpdateCharacter(ctx context.Context, p *character.Character) (res *character.Character, err error) { - res = &character.Character{} s.logger.Print("character.updateCharacter") + + itemToUpdate := s.characters[*p.ID] + if itemToUpdate == nil { + s.logger.Printf("characters with id %d not found", &p.ID) + return nil, errors.New("characters not found") + } + s.characters[*p.ID] = p + res = s.characters[*p.ID] + return } // DeleteCharacter implements deleteCharacter. -func (s *charactersrvc) DeleteCharacter(ctx context.Context, p int) (err error) { +func (s *charactersrvc) DeleteCharacter(ctx context.Context, p *character.DeleteCharacterPayload) (err error) { s.logger.Print("character.deleteCharacter") + + itemToDelete := s.characters[*p.ID] + if itemToDelete == nil { + s.logger.Printf("characters with id %d not found", &p.ID) + return errors.New("characters not found") + } + + delete(s.characters, *p.ID) + return } diff --git a/services/character/design/design.go b/services/character/design/design.go index 33fa0dc..749bcc9 100644 --- a/services/character/design/design.go +++ b/services/character/design/design.go @@ -9,17 +9,19 @@ import ( var _ = API("character", func() { Title("Character Srvice") Server("character", func() { - Host("localhost", func() { - URI("grpc://localhost:8083") - }) - }) + Host("localhost", func() { + URI("grpc://localhost:8083") + }) + }) }) var _ = Service("character", func() { Description("A GRPC back service that handles CRUD operations for the characters and their attributes") Method("getCharacter", func() { - Payload(Int) + Payload(func() { + Field(1, "id", Int) + }) Result(Character) Error("NotFound") Error("BadRequest") @@ -61,7 +63,9 @@ var _ = Service("character", func() { }) Method("deleteCharacter", func() { - Payload(Int) + Payload(func() { + Field(1, "id", Int) + }) Result(Empty) Error("NotFound") Error("BadRequest") diff --git a/services/front/cmd/front/main.go b/services/front/cmd/front/main.go index edf66ce..6372758 100644 --- a/services/front/cmd/front/main.go +++ b/services/front/cmd/front/main.go @@ -21,14 +21,14 @@ func main() { // Define command line flags, add any other flag required to configure the // service. var ( - hostF = flag.String("host", "localhost", "Server host (valid values: localhost)") - domainF = flag.String("domain", "", "Host domain name (overrides host domain specified in service design)") - httpPortF = flag.String("http-port", "", "HTTP port (overrides host HTTP port specified in service design)") - secureF = flag.Bool("secure", false, "Use secure scheme (https or grpcs)") - dbgF = flag.Bool("debug", false, "Log request and response bodies") - itemAddr = flag.String("locator-addr", ":8082", "Item service address") - //characterAddr = flag.String("locator-addr", ":8080", "Item service address") - //inventoryAddr = flag.String("locator-addr", ":8081", "Item service address") + hostF = flag.String("host", "localhost", "Server host (valid values: localhost)") + domainF = flag.String("domain", "", "Host domain name (overrides host domain specified in service design)") + httpPortF = flag.String("http-port", "", "HTTP port (overrides host HTTP port specified in service design)") + secureF = flag.Bool("secure", false, "Use secure scheme (https or grpcs)") + dbgF = flag.Bool("debug", false, "Log request and response bodies") + itemAddr = flag.String("item-addr", ":8082", "Item service address") + characterAddr = flag.String("character-addr", ":8083", "Character service address") + inventoryAddr = flag.String("inventory-addr", ":8081", "Inventory service address") ) flag.Parse() @@ -49,12 +49,29 @@ func main() { } defer itemClientConnection.Close() + inventoryClientConnection, err := grpc.Dial(*inventoryAddr, + grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Fatalf("failed to connect to inventory service", err) + os.Exit(1) + } + defer inventoryClientConnection.Close() + + characterClientConnection, err := grpc.Dial(*characterAddr, + grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Fatalf("failed to connect to character service", err) + os.Exit(1) + } + defer characterClientConnection.Close() + // Initialize the services. var ( frontSvc front.Service ) { - frontSvc = frontapi.NewFront(logger, itemClientConnection) + frontSvc = frontapi.NewFront(logger, itemClientConnection, characterClientConnection, + inventoryClientConnection) } // Wrap the services in endpoints that can be invoked from other services diff --git a/services/front/design/design.go b/services/front/design/design.go index 5302011..22d6396 100644 --- a/services/front/design/design.go +++ b/services/front/design/design.go @@ -100,7 +100,6 @@ var _ = Service("front", func() { HTTP(func() { POST("/character") - Body(Character) Response(StatusBadRequest) Response(StatusOK) Response(StatusInternalServerError) @@ -115,7 +114,6 @@ var _ = Service("front", func() { HTTP(func() { PUT("/character/{id}") - Body(Character) Response(StatusOK) Response(StatusBadRequest) Response(StatusNotFound) @@ -129,7 +127,7 @@ var _ = Service("front", func() { Error("BadRequest") HTTP(func() { - POST("/character/{id}") + DELETE("/character/{id}") Response(StatusOK) Response(StatusBadRequest) Response(StatusNotFound) @@ -137,26 +135,26 @@ var _ = Service("front", func() { }) Method("addItemToInventory", func() { - Payload(Int) + Payload(InventoryRecord) Result(Empty) Error("NotFound") Error("BadRequest") HTTP(func() { - POST("/inventory/{CharacterId}") + POST("/character/{characterId}/item") Response(StatusOK) Response(StatusBadRequest) }) }) Method("removeItemFromInventory", func() { - Payload(Int) + Payload(InventoryRecord) Result(Empty) Error("NotFound") Error("BadRequest") HTTP(func() { - PUT("/inventory/{CharacterId}") + DELETE("/character/{characterId}/item/{itemId}") Response(StatusOK) Response(StatusBadRequest) }) diff --git a/services/front/front.go b/services/front/front.go index 6141df2..fa6aa82 100644 --- a/services/front/front.go +++ b/services/front/front.go @@ -2,7 +2,11 @@ package frontapi import ( "context" + genCharacter "crossnokaye-interview-assignment/services/character/gen/character" + genCharacterClient "crossnokaye-interview-assignment/services/character/gen/grpc/character/client" front "crossnokaye-interview-assignment/services/front/gen/front" + genInventoryClient "crossnokaye-interview-assignment/services/inventory/gen/grpc/inventory/client" + genInventory "crossnokaye-interview-assignment/services/inventory/gen/inventory" genItemClient "crossnokaye-interview-assignment/services/item/gen/grpc/item/client" genItem "crossnokaye-interview-assignment/services/item/gen/item" goa "goa.design/goa/v3/pkg" @@ -17,24 +21,50 @@ type itemClient struct { deleteItem goa.Endpoint } +type characterClient struct { + getCharacter goa.Endpoint + createCharacter goa.Endpoint + updateCharacter goa.Endpoint + deleteCharacter goa.Endpoint +} + +type inventoryClient struct { + addItem goa.Endpoint + removeItem goa.Endpoint +} + // front service example implementation. // The example methods log the requests and return zero values. type frontsrvc struct { logger *log.Logger - itemClient *itemClient + itemClient *itemClient + characterClient *characterClient + inventoryClient *inventoryClient } // NewFront returns the front service implementation. -func NewFront(logger *log.Logger, itemClientConnection *grpc.ClientConn) front.Service { +func NewFront(logger *log.Logger, itemClientConnection *grpc.ClientConn, characterClientConnection *grpc.ClientConn, + inventoryClientConnection *grpc.ClientConn) front.Service { ic := genItemClient.NewClient(itemClientConnection) - return &frontsrvc{logger: logger, itemClient: &itemClient{ - ic.GetItem(), - ic.CreateItem(), - ic.UpdateItem(), - ic.DeleteItem()}} - + cc := genCharacterClient.NewClient(characterClientConnection) + icc := genInventoryClient.NewClient(inventoryClientConnection) + return &frontsrvc{logger: logger, + itemClient: &itemClient{ + ic.GetItem(), + ic.CreateItem(), + ic.UpdateItem(), + ic.DeleteItem()}, + characterClient: &characterClient{ + cc.GetCharacter(), + cc.CreateCharacter(), + cc.UpdateCharacter(), + cc.DeleteCharacter()}, + inventoryClient: &inventoryClient{ + icc.AddItem(), + icc.RemoveItem(), + }} } // GetItem implements getItem. @@ -86,40 +116,73 @@ func (s *frontsrvc) DeleteItem(ctx context.Context, p int) (err error) { } // GetCharacter implements getCharacter. -func (s *frontsrvc) GetCharacter(ctx context.Context, p int) (res *front.Character, err error) { - res = &front.Character{} +func (s *frontsrvc) GetCharacter(ctx context.Context, id int) (res *front.Character, err error) { s.logger.Print("front.getCharacter") + getCharacterResponse, err := s.characterClient.getCharacter(ctx, &genCharacter.GetCharacterPayload{ID: &id}) + if err != nil { + return nil, err + } + character := getCharacterResponse.(*genCharacter.Character) + res = (*front.Character)(character) return } // CreateCharacter implements createCharacter. func (s *frontsrvc) CreateCharacter(ctx context.Context, p *front.Character) (res *front.Character, err error) { - res = &front.Character{} s.logger.Print("front.createCharacter") + createCharacterResponse, err := s.characterClient.createCharacter(ctx, (*genCharacter.Character)(p)) + if err != nil { + return nil, err + } + character := createCharacterResponse.(*genCharacter.Character) + res = (*front.Character)(character) return } // UpdateCharacter implements updateCharacter. func (s *frontsrvc) UpdateCharacter(ctx context.Context, p *front.Character) (res *front.Character, err error) { - res = &front.Character{} s.logger.Print("front.updateCharacter") + updateCharacterResponse, err := s.characterClient.updateCharacter(ctx, (*genCharacter.Character)(p)) + if err != nil { + return nil, err + } + + character := updateCharacterResponse.(*genCharacter.Character) + res = (*front.Character)(character) + return } // DeleteCharacter implements deleteCharacter. func (s *frontsrvc) DeleteCharacter(ctx context.Context, p int) (err error) { s.logger.Print("front.deleteCharacter") + + _, err = s.characterClient.deleteCharacter(ctx, &genCharacter.DeleteCharacterPayload{ID: &p}) + if err != nil { + return err + } + return } // AddItemToInventory implements addItemToInventory. -func (s *frontsrvc) AddItemToInventory(ctx context.Context, p int) (err error) { +func (s *frontsrvc) AddItemToInventory(ctx context.Context, p *front.InventoryRecord) (err error) { s.logger.Print("front.addItemToInventory") + _, err = s.inventoryClient.addItem(ctx, (*genInventory.InventoryRecord)(p)) + if err != nil { + return err + } + return } // RemoveItemFromInventory implements removeItemFromInventory. -func (s *frontsrvc) RemoveItemFromInventory(ctx context.Context, p int) (err error) { +func (s *frontsrvc) RemoveItemFromInventory(ctx context.Context, p *front.InventoryRecord) (err error) { s.logger.Print("front.removeItemFromInventory") + _, err = s.inventoryClient.removeItem(ctx, (*genInventory.InventoryRecord)(p)) + if err != nil { + return err + } + return } diff --git a/services/inventory/design/design.go b/services/inventory/design/design.go index 1d83cc6..3935e8e 100644 --- a/services/inventory/design/design.go +++ b/services/inventory/design/design.go @@ -1,16 +1,17 @@ package design import ( + "crossnokaye-interview-assignment/design" . "goa.design/goa/v3/dsl" ) var _ = API("inventory", func() { Title("Inventory Service") Server("inventory", func() { - Host("localhost", func() { - URI("grpc://localhost:8081") - }) - }) + Host("localhost", func() { + URI("grpc://localhost:8081") + }) + }) }) var _ = Service("inventory", func() { @@ -19,7 +20,7 @@ var _ = Service("inventory", func() { // }) Method("addItem", func() { - Payload(Int) + Payload(design.InventoryRecord) Result(Empty) Error("NotFound") Error("BadRequest") @@ -30,7 +31,7 @@ var _ = Service("inventory", func() { }) Method("removeItem", func() { - Payload(Int) + Payload(design.InventoryRecord) Result(Empty) Error("NotFound") Error("BadRequest") diff --git a/services/inventory/inventory.go b/services/inventory/inventory.go index 5ef1a54..7174c79 100644 --- a/services/inventory/inventory.go +++ b/services/inventory/inventory.go @@ -3,28 +3,76 @@ package inventoryapi import ( "context" inventory "crossnokaye-interview-assignment/services/inventory/gen/inventory" + "errors" "log" ) // inventory service example implementation. // The example methods log the requests and return zero values. type inventorysrvc struct { - logger *log.Logger + logger *log.Logger + inventories map[int]*[]int //key = characterId, value = array of itemIds } // NewInventory returns the inventory service implementation. func NewInventory(logger *log.Logger) inventory.Service { - return &inventorysrvc{logger} + inventoryMap := make(map[int]*[]int) + return &inventorysrvc{logger, inventoryMap} } // AddItem implements addItem. -func (s *inventorysrvc) AddItem(ctx context.Context, p int) (err error) { +func (s *inventorysrvc) AddItem(ctx context.Context, p *inventory.InventoryRecord) (err error) { s.logger.Print("inventory.addItem") + + itemList := s.inventories[*p.CharacterID] + if itemList == nil { + itemList = s.initCharacterInventory(p.CharacterID) + } + newItemList := append(*itemList, *p.ItemID) + s.inventories[*p.CharacterID] = &newItemList + return } // RemoveItem implements removeItem. -func (s *inventorysrvc) RemoveItem(ctx context.Context, p int) (err error) { +func (s *inventorysrvc) RemoveItem(ctx context.Context, p *inventory.InventoryRecord) (err error) { s.logger.Print("inventory.removeItem") + + itemList := s.inventories[*p.CharacterID] + if itemList == nil { + s.logger.Printf("inventory for character with id %d not found", p.CharacterID) + return errors.New("item not found") + } + + idx := indexOf(*p.ItemID, *itemList) + if idx != -1 { + newItemList := remove(*itemList, idx) + s.inventories[*p.CharacterID] = &newItemList + } else { + s.logger.Printf("character with id %d does not have item %d in inventory", &p.CharacterID, &p.ItemID) + return errors.New("item not found for character") + } + return } + +func (s *inventorysrvc) initCharacterInventory(characterId *int) (itemList *[]int) { + list := make([]int, 0) + itemList = &list + s.inventories[*characterId] = &list + return +} + +func indexOf(element int, data []int) int { + for k, v := range data { + if element == v { + return k + } + } + return -1 //not found. +} + +func remove(s []int, i int) []int { + s[i] = s[len(s)-1] + return s[:len(s)-1] +} diff --git a/services/item/item.go b/services/item/item.go index 1e50815..41dc872 100644 --- a/services/item/item.go +++ b/services/item/item.go @@ -26,7 +26,7 @@ func (s *itemsrvc) GetItem(ctx context.Context, p *item.GetItemPayload) (res *it itemToGet := s.items[*p.ID] if itemToGet == nil { - s.logger.Printf("itemToGet with id %d not found", p.ID) + s.logger.Printf("item with id %d not found", &p.ID) return nil, errors.New("item not found") } res = s.items[*p.ID] @@ -59,7 +59,7 @@ func (s *itemsrvc) UpdateItem(ctx context.Context, p *item.Item) (res *item.Item s.logger.Print("itemToGet.updateItem") itemToUpdate := s.items[*p.ID] if itemToUpdate == nil { - s.logger.Printf("itemToGet with id %d not found", p.ID) + s.logger.Printf("item with id %d not found", &p.ID) return nil, errors.New("item not found") } s.items[*p.ID] = p @@ -73,7 +73,7 @@ func (s *itemsrvc) DeleteItem(ctx context.Context, p *item.DeleteItemPayload) (e s.logger.Print("item.deleteItem") itemToDelete := s.items[*p.ID] if itemToDelete == nil { - s.logger.Printf("itemToGet with id %d not found", p.ID) + s.logger.Printf("item with id %d not found", &p.ID) return errors.New("item not found") }