diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12a640a..42785c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,8 +11,8 @@ on: - "**.md" env: - GO_VERSION: "1.22.5" # https://go.dev/dl/ - GOLANGCI_LINT_VERSION: "v1.59.1" # https://github.com/golangci/golangci-lint/releases + GO_VERSION: "1.23.4" # https://go.dev/dl/ + GOLANGCI_LINT_VERSION: "v1.62.2" # https://github.com/golangci/golangci-lint/releases jobs: lint: @@ -27,7 +27,7 @@ jobs: with: version: ${{ env.GOLANGCI_LINT_VERSION }} skip-cache: true - working-directory: ./ios-engineer/server + working-directory: ./client-challenge/server security: runs-on: ubuntu-latest @@ -35,7 +35,7 @@ jobs: - uses: golang/govulncheck-action@v1 with: go-version-input: ${{ env.GO_VERSION }} - work-dir: ./ios-engineer/server + work-dir: ./client-challenge/server build: runs-on: ubuntu-latest @@ -45,5 +45,5 @@ jobs: with: go-version: ${{ env.GO_VERSION }} - name: Build go code - working-directory: ./ios-engineer/server + working-directory: ./client-challenge/server run: go build diff --git a/backend-engineer/readme.md b/backend-challenge/readme.md similarity index 88% rename from backend-engineer/readme.md rename to backend-challenge/readme.md index ae75a3b..7c33a65 100644 --- a/backend-engineer/readme.md +++ b/backend-challenge/readme.md @@ -1,7 +1,7 @@ # Backend Challenge -This challenge is part of the backend hiring process at [Heart -Hands](https://hearthands.tech/). +_This challenge is part of the engineering hiring process at [Heart +Hands](https://hearthands.tech/)._ ## Why this challenge? @@ -11,8 +11,9 @@ understanding of their technical stack, and excellent product intuitions to join our team. This exercise has been designed to give a glimpse of what it is like to build a -messaging app, and the kind of technical challenges we face and care about. We -are expecting you to spend between 4 and 6 hours on this challenge. +messaging app, and the kind of technical challenges we face and care about. + +We expect you to spend 4-6 hours on this challenge, simulating real-world, time-boxed work. ## Instructions @@ -50,7 +51,7 @@ will have to prioritize what you work on. A few things that are important for us and that will be considered during the review: - **documentation**: is the readme clear? are important parts of the code documented? -- **impact**: which features did you prioritize? +- **impact**: what did you consciouslly decided to prioritize? - **maintainability**: is the code well-structured and easy to read/evolve? - **robustness**: is the code tested or easily testable? are edge-cases considered? is static analysis leveraged? diff --git a/ios-engineer/design.png b/client-challenge/design.png similarity index 100% rename from ios-engineer/design.png rename to client-challenge/design.png diff --git a/ios-engineer/readme.md b/client-challenge/readme.md similarity index 70% rename from ios-engineer/readme.md rename to client-challenge/readme.md index 23f92f0..b98e60d 100644 --- a/ios-engineer/readme.md +++ b/client-challenge/readme.md @@ -1,7 +1,7 @@ -# iOS Challenge +# Client Challenge -This challenge is part of the iOS hiring process at [Heart -Hands](https://hearthands.tech/). +_This challenge is part of the engineering hiring process at [Heart +Hands](https://hearthands.tech/)._ ## Why this challenge? @@ -11,19 +11,24 @@ understanding of their technical stack, and excellent product intuitions to join our team. This exercise has been designed to give a glimpse of what it is like to build a -messaging app, and the kind of technical challenges we face and care about. We -are expecting you to spend between 4 and 6 hours on this challenge. +messaging app, and the kind of technical challenges we face and care about. + +We expect you to spend 4-6 hours on this challenge, simulating real-world, +time-boxed work. ## Instructions -You are tasked with the implementation of an iOS messaging app that allows the user to -communicate (send and receive text messages) with bots, each in their own 1:1 +You are tasked with the implementation of a messaging app that allows the user +to communicate (send and receive text messages) with bots, each in their own 1:1 chat. -A server is available for you to use. You can read more about it in +A companion server is available for you to use. You can read more about it in [`./server`](./server). Its documentation contains information on how it can be run, and what kinds of API endpoints & entities are available. +You can choose a target platform of your choice for this challenge: iOS, macOS, +or web. + Functional requirements: - [ ] The app should start on a screen showing the list of all chats @@ -36,14 +41,14 @@ Functional requirements: Some topics that we find interesting to dig: -- [ ] Make the app work offline (both for app state and sending) +- [ ] Make the app work offline using a persistence layer of your choice - [ ] Make the app idempotent in regards to what you send and receive - [ ] Integrate a splashscreen to hide chats while the app is loading - [ ] Add support for optimistic sending to give instantaneity in the UI - [ ] Add support for a local read/unread indicator -- [ ] Avoid block changing states so the app feels fluid & snappy -- [ ] Make the app compatible to run on iPad and macOS -- [ ] Make the app runnable on multiple devices +- [ ] Avoid to block changing states so the app feels fluid & snappy +- [ ] Make the app available in multiple environments (e.g.: iOS/macOS, + web/electron) - [ ] _Anything_ that you feel could improve the UX! ## Design @@ -60,7 +65,7 @@ will have to prioritize what you work on. A few things that are important for us and that will be considered during the review: - **documentation**: is the readme clear? are important parts of the code documented? -- **impact**: which features did you prioritize? +- **impact**: what did you consciouslly decided to prioritize? - **maintainability**: is the code well-structured and easy to read/evolve? - **robustness**: is the code tested or easily testable? are edge-cases considered? is static analysis leveraged? diff --git a/ios-engineer/server/.gitignore b/client-challenge/server/.gitignore similarity index 100% rename from ios-engineer/server/.gitignore rename to client-challenge/server/.gitignore diff --git a/client-challenge/server/data.json b/client-challenge/server/data.json new file mode 100644 index 0000000..078a0d8 --- /dev/null +++ b/client-challenge/server/data.json @@ -0,0 +1,15 @@ +[ + "I told my doctor that I broke my arm in two places – he told me to stop going to those places.", + "This is your captain speaking, AND THIS IS YOUR CAPTAIN SHOUTING.", + "Before you criticize someone, walk a mile in their shoes. That way, when you do criticize them, you're a mile away, and you have their shoes.", + "I was at the park wondering why this frisbee kept getting bigger… and then it hit me.", + "Two fish in a tank, one looks at the other and says, 'How do you drive this thing?'", + "Evening news is where they begin with 'Good evening,' and then proceed to tell you why it isn’t.", + "When I met my now wife, I asked if she was vegetarian because she really loved animals. She responded, 'No, I just really hate vegetables.'", + "I know they say that money talks, but all mine says is 'Goodbye.'", + "I have an inferiority complex, but it's not a very good one.", + "What do you call a lazy kangaroo? A pouch potato.", + "My wife and I laugh about how competitive we are. But I laugh more.", + "My wife told me to stop impersonating a flamingo. I had to put my foot down.", + "Have you heard about the guy who stole the calendar?! Well, he got 12 months!" +] diff --git a/ios-engineer/server/go.mod b/client-challenge/server/go.mod similarity index 62% rename from ios-engineer/server/go.mod rename to client-challenge/server/go.mod index 451f351..aeef84b 100644 --- a/ios-engineer/server/go.mod +++ b/client-challenge/server/go.mod @@ -3,14 +3,15 @@ module github.com/hearthands/challenges/ios-engineer/server go 1.22.5 require ( - github.com/go-chi/chi/v5 v5.1.0 + github.com/go-chi/chi/v5 v5.2.0 github.com/go-chi/render v1.0.3 + github.com/google/uuid v1.6.0 github.com/r3labs/sse/v2 v2.10.0 - golang.org/x/exp v0.0.0-20240707233637-46b078467d37 + golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 ) require ( github.com/ajg/form v1.5.1 // indirect - golang.org/x/net v0.27.0 // indirect + golang.org/x/net v0.33.0 // indirect gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect ) diff --git a/ios-engineer/server/go.sum b/client-challenge/server/go.sum similarity index 75% rename from ios-engineer/server/go.sum rename to client-challenge/server/go.sum index 3df15b6..8c3c74e 100644 --- a/ios-engineer/server/go.sum +++ b/client-challenge/server/go.sum @@ -2,10 +2,12 @@ github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= -github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= +github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= @@ -14,11 +16,11 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w= -golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo= +golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= diff --git a/ios-engineer/server/main.go b/client-challenge/server/main.go similarity index 66% rename from ios-engineer/server/main.go rename to client-challenge/server/main.go index 44feffb..0d36e28 100644 --- a/ios-engineer/server/main.go +++ b/client-challenge/server/main.go @@ -1,6 +1,7 @@ package main import ( + "cmp" _ "embed" "encoding/json" "fmt" @@ -8,28 +9,28 @@ import ( "net" "net/http" "os" - "strconv" - "strings" "sync" - "sync/atomic" "time" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/render" + "github.com/google/uuid" "github.com/r3labs/sse/v2" "golang.org/x/exp/maps" ) +const ( + // maximum number of entities returned in a single request + limit = 100 +) + var ( // hostname used by the server - hostname = envOrDefault("HTTP_HOST", "localhost") + hostname = cmp.Or(os.Getenv("HTTP_HOST"), "localhost") // port used by the server - port = envOrDefault("PORT", "3000") - - // maximum number of entities returned in a single request - limit = 100 + port = cmp.Or(os.Getenv("PORT"), "3000") ) //go:embed data.json @@ -43,7 +44,7 @@ func main() { r.Use(middleware.Logger) r.Use(middleware.Recoverer) - r.Use(middleware.Timeout(60 * time.Second)) + r.Use(middleware.Timeout(30 * time.Second)) r.Handle("/events", http.HandlerFunc(events.ServeHTTP)) @@ -51,14 +52,10 @@ func main() { r.Use(render.SetContentType(render.ContentTypeJSON)) r.Use(chaosMiddleware) r.Get("/chats", app.GetChats) - r.Get("/chats/{chatID}/messages", app.GetMessages) - r.Post("/chats/{chatID}/messages", app.PostMessages) + r.Get("/chats/{chatID}/messages", app.GetChatMessages) + r.Post("/chats/{chatID}/messages", app.PostChatMessages) }) - r.Get("/livez", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - })) - addr := net.JoinHostPort(hostname, port) fmt.Printf("Starting server on http://%s\n", addr) if err := http.ListenAndServe(addr, r); err != nil { @@ -71,18 +68,19 @@ func main() { // type Chat struct { - ID uint32 `json:"id"` - Name string `json:"name"` + ID uuid.UUID `json:"id"` + Name string `json:"name"` messages []*Message } type Message struct { - ID uint32 `json:"id"` - ChatID uint32 `json:"chat_id"` - Author string `json:"author"` - Text string `json:"text"` - SentAt time.Time `json:"sent_at"` + ID uuid.UUID `json:"id"` + ChatID uuid.UUID `json:"chat_id"` + Author string `json:"author"` + Text string `json:"text"` + SentAt time.Time `json:"sent_at"` + IdempotencyKey string `json:"idempotency_key"` } // App is both our controller and our data store. This coupling allows to keep @@ -93,7 +91,7 @@ type App struct { mu sync.RWMutex idempotencyKeys map[string]struct{} // store idempotency keys - store map[uint32]*Chat // in-memory store of chats and messages + store map[uuid.UUID]*Chat // in-memory store of chats and messages } func NewApp(events *sse.Server) *App { @@ -107,22 +105,21 @@ func NewApp(events *sse.Server) *App { now := time.Now() // store contains all the chats, indexed by their ID - store := map[uint32]*Chat{} - for i, chat := range []*Chat{ - {Name: "John", messages: []*Message{ - {ID: newID(), Author: "bot", Text: "Sounds good 👍", SentAt: now}, + store := map[uuid.UUID]*Chat{} + for _, chat := range []*Chat{ + {ID: uuid.New(), Name: "John", messages: []*Message{ + {ID: uuid.New(), Author: "bot", Text: "Sounds good 👍", SentAt: now, IdempotencyKey: uuid.New().String()}, }}, - {Name: "Jessica", messages: []*Message{ - {ID: newID(), Author: "bot", Text: "How are you!?", SentAt: now.Add(-1 * time.Minute)}, + {ID: uuid.New(), Name: "Jessica", messages: []*Message{ + {ID: uuid.New(), Author: "bot", Text: "How are you!?", SentAt: now.Add(-1 * time.Minute), IdempotencyKey: uuid.New().String()}, }}, - {Name: "Matt", messages: []*Message{ - {ID: newID(), Author: "bot", Text: "ok chat soon :)", SentAt: now.Add(-32 * time.Minute)}, + {ID: uuid.New(), Name: "Matt", messages: []*Message{ + {ID: uuid.New(), Author: "bot", Text: "ok chat soon :)", SentAt: now.Add(-32 * time.Minute), IdempotencyKey: uuid.New().String()}, }}, - {Name: "Sarah", messages: []*Message{ - {ID: newID(), Author: "bot", Text: "ok talk later!", SentAt: now.Add(-24 * time.Hour)}, + {ID: uuid.New(), Name: "Sarah", messages: []*Message{ + {ID: uuid.New(), Author: "bot", Text: "ok talk later!", SentAt: now.Add(-24 * time.Hour), IdempotencyKey: uuid.New().String()}, }}, } { - chat.ID = uint32(i + 1) // we don't want to have a chat id == 0 for _, message := range chat.messages { message.ChatID = chat.ID } @@ -147,7 +144,7 @@ func (app *App) GetChats(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, chats) } -func (app *App) GetMessages(w http.ResponseWriter, r *http.Request) { +func (app *App) GetChatMessages(w http.ResponseWriter, r *http.Request) { app.mu.RLock() defer app.mu.RUnlock() @@ -164,7 +161,7 @@ func (app *App) GetMessages(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, chat.messages[start:end]) } -func (app *App) PostMessages(w http.ResponseWriter, r *http.Request) { +func (app *App) PostChatMessages(w http.ResponseWriter, r *http.Request) { app.mu.Lock() defer app.mu.Unlock() @@ -192,18 +189,21 @@ func (app *App) PostMessages(w http.ResponseWriter, r *http.Request) { } newMessage := &Message{ - ID: newID(), - ChatID: chat.ID, - Author: "user", - Text: message.Text, - SentAt: time.Now(), + ID: uuid.New(), + ChatID: chat.ID, + Author: "user", + Text: message.Text, + SentAt: time.Now(), + IdempotencyKey: idempotencyKey, } chat.messages = append(chat.messages, newMessage) app.publishEvent("messages", newMessage) - app.idempotencyKeys[idempotencyKey] = struct{}{} + go app.sendDelayedAnswer(chat) + app.idempotencyKeys[idempotencyKey] = struct{}{} + w.WriteHeader(http.StatusOK) render.JSON(w, r, newMessage) } @@ -217,11 +217,12 @@ func (app *App) sendDelayedAnswer(chat *Chat) { text := app.jokes[rand.Intn(len(app.jokes))] newMessage := &Message{ - ID: newID(), - ChatID: chat.ID, - Author: "bot", - Text: text, - SentAt: time.Now(), + ID: uuid.New(), + ChatID: chat.ID, + Author: "bot", + Text: text, + SentAt: time.Now(), + IdempotencyKey: uuid.New().String(), } chat.messages = append(chat.messages, newMessage) @@ -229,14 +230,14 @@ func (app *App) sendDelayedAnswer(chat *Chat) { } func (app *App) findChatByID(rawID string) (*Chat, error) { - chatID, err := strconv.ParseUint(rawID, 10, 32) + parsedID, err := uuid.Parse(rawID) if err != nil { return nil, fmt.Errorf("invalid chat ID %s: %w", rawID, err) } - chat, ok := app.store[uint32(chatID)] + chat, ok := app.store[parsedID] if !ok { - return nil, fmt.Errorf("chat %s not found", rawID) + return nil, fmt.Errorf("chat not found with ID %s", rawID) } return chat, nil @@ -253,28 +254,6 @@ func (app *App) publishEvent(stream string, payload any) { } } -// -// Helpers -// - -// lastID is the last ID generated by newID. It starts counting from the current -// timestamp in seconds. -var lastID = uint32(time.Now().Unix()) - -// newID generates a new ID, unique for the lifetime of this server. -func newID() uint32 { - return atomic.AddUint32(&lastID, 1) -} - -// envOrDefault returns the value of the environment variable at the given key. -// Fallbacks to the given default if the value found is missing or empty. -func envOrDefault(key string, defaultValue string) string { - if v := strings.TrimSpace(os.Getenv(key)); len(v) > 0 { - return v - } - return defaultValue -} - // chaosMiddleware makes sure to randomly return errors and adds artificial // latency to simulate a real world system. func chaosMiddleware(next http.Handler) http.Handler { @@ -288,7 +267,7 @@ func chaosMiddleware(next http.Handler) http.Handler { return } - // 5% chance of timeout + // 5% chance of timeout after 5 seconds if rand.Intn(100) < 5 { <-time.After(5 * time.Second) http.Error(w, "timeout", http.StatusGatewayTimeout) diff --git a/ios-engineer/server/readme.md b/client-challenge/server/readme.md similarity index 69% rename from ios-engineer/server/readme.md rename to client-challenge/server/readme.md index 087daac..3425e33 100644 --- a/ios-engineer/server/readme.md +++ b/client-challenge/server/readme.md @@ -1,7 +1,6 @@ -# iOS Challenge - Server Companion +# Client Challenge - Server Companion -This server is used as a companion for the iOS challenge. Persistence is -in-memory, and restarting the server will reset its state. +This server is used as a companion for the client challenge. The server will sometime be a bit capricious, you might observe: - failure to receive a response @@ -10,6 +9,9 @@ The server will sometime be a bit capricious, you might observe: When that happens, you should retry and react accordingly. +_Note: persistence is in-memory only, and restarting the server will reset its +state._ + ## How to start the server? 1. You need to install Go on your system: follow the [official documentation](https://go.dev/doc/install) @@ -17,12 +19,14 @@ When that happens, you should retry and react accordingly. ## How to make the server reachable from another machine? -You might want to allow the server to be reached from another machine -(simulator or a phone for example). One way to do that is to use [ngrok](). +You might want to allow the server to be reached from another machine (a +simulator or a phone for example). One way to do that is to use +[ngrok](https://ngrok.com/). 1. You need to install Ngrok on your system: follow the [official documentation](https://ngrok.com/download) -2. Then, after having started the server (see above), execute the command: `ngrok http 3000` +2. Then, after having started the go server (see above), execute the command + `ngrok http 3000`. This will give you a public URL of the form `https://9f98-62-194-145-77.ngrok.io`, that can be used to reach your server @@ -42,12 +46,15 @@ chats, and read & write messages. ### Endpoints -- `GET /events?stream=messages`: an [SSE](https://en.wikipedia.org/wiki/Server-sent_events) stream that sends you `Message` entities as they are received by the server +- `GET /events?stream=messages`: an + [SSE](https://en.wikipedia.org/wiki/Server-sent_events) stream that sends you + `Message` entities as they are ingested by the server - `GET /chats`: returns a list of all `Chat` entities -- `GET /chats/{chat_id}/messages`: returns a list of the 100 most recent `Message` entities in a chat -- `POST /chats/{chat_id}/messages`: send a new message in a chat and returns - the newly created `Message` entity. It expects a JSON payload of the form: `{ "text": - "..." }` +- `GET /chats/{chat_id}/messages`: returns a list of the 100 most recent + `Message` entities in a chat +- `POST /chats/{chat_id}/messages`: send a new message in a chat and returns the + newly created `Message` entity. It expects a JSON payload of the form: `{ + "text": "..." }` ### Entities @@ -58,6 +65,9 @@ chats, and read & write messages. #### `Message` - `id` (`number`): the message id - `chat_id` (`number`): the id of the chat this message belongs to -- `author` (`string`): the message author (either the constant string `"user"` or `"bot"`) +- `author` (`string`): the message author (either the constant string `"user"` + or `"bot"`) - `text` (`string`): the actual content of the message - `sent_at` (`string`): the date at which the message was sent +- `idempotency_key` (`string`): the idempotency key that was passed during the + message creation diff --git a/design-engineer/readme.md b/design-challenge/readme.md similarity index 82% rename from design-engineer/readme.md rename to design-challenge/readme.md index 8e722af..93e4c8e 100644 --- a/design-engineer/readme.md +++ b/design-challenge/readme.md @@ -1,8 +1,7 @@ -# Design Engineer Challenge +# Design Challenge -Welcome to the Heart Hands Design Engineer Challenge. We want to see your creativity, product instincts, strategic thinking, and technical skills in action. - -This challenge is part of the hiring process for Design Engineers at [Heart Hands](https://hearthands.tech). +_This challenge is part of the engineering hiring process at [Heart +Hands](https://hearthands.tech/)._ ## Why this challenge? @@ -42,9 +41,7 @@ Prototype with code: If you have ideas that are better expressed through code th We have prepared a Figma file containing some of our existing designs for you to copy or build upon! -[Open Figma](https://www.figma.com/design/LYHc8FzoYfAVo2tPdh9lc1/%5BStef%5D-Design-Engineer-Challenge?node-id=0-1&t=bPSKWg0mJOr1aq31-1) - -Screenshot 2024-10-11 at 1 28 54 PM + ## Challenge Review diff --git a/ios-engineer/server/data.json b/ios-engineer/server/data.json deleted file mode 100644 index fb7f833..0000000 --- a/ios-engineer/server/data.json +++ /dev/null @@ -1,26 +0,0 @@ -[ - "How did the doctor revive the developer?", - "How did the web dev hurt Comic Sans feelings?", - "How do you comfort a JavaScript bug?", - "How do you make a Web App accessible?", - "What do you call __proto__? Dunder proto. Michael Scott was the regional manager where?", - "What does a React proposal mean?", - "Why couldn’t the React component understand the joke?", - "Why did Jason cover himself with bubble wrap?", - "Why did the C# developer fall asleep?", - "Why did the CoffeeScript developer keep getting lost?", - "Why did the JavaScript boxer goto the chiropractor?", - "Why did the React Higher Order Component give up?", - "Why did the Web A11y Dev keep getting distracted?", - "Why did the child component have such great self-esteem?", - "Why did the developer go broke?", - "Why did the functional component feel lost?", - "Why did the react class component feel relieved?", - "Why did the react developer have an addiction?", - "Why did the software company hire drama majors from Starbucks?", - "Why do C# and Java developers keep breaking their keyboards", - "Why was Ember.js turning red?", - "Why was the JavaScript developer sad?", - "Why was the react developer late to everything?", - "dev1 > What tool do you use to switch versions of node?" -] diff --git a/readme.md b/readme.md index 8b209ab..d2aee20 100644 --- a/readme.md +++ b/readme.md @@ -1,8 +1,6 @@ # Challenges [![ci status](https://github.com/hearthandsinc/challenges/actions/workflows/ci.yml/badge.svg)](https://github.com/hearthandsinc/challenges/actions/workflows/ci.yml) -This repository contains the [Heart Hands](https://hearthands.tech/) challenges -for the [iOS](./ios-engineer), [design](./design-engineer), and -[backend](./backend-engineer) engineers positions. +This repository contains the [Heart Hands](https://hearthands.tech/) engineering challenges ([client](./client-challenge), [design](./design-challenge), and [backend](./backend-challenge)). ## Feedbacks