diff --git a/.github/workflows/tour_of_beam_backend.yml b/.github/workflows/tour_of_beam_backend.yml index 7182911b3c20..c87f962cc394 100644 --- a/.github/workflows/tour_of_beam_backend.yml +++ b/.github/workflows/tour_of_beam_backend.yml @@ -23,6 +23,7 @@ on: push: branches: ['master', 'release-*'] tags: 'v*' + paths: ['learning/tour-of-beam/backend/**'] pull_request: branches: ['master', 'release-*'] tags: 'v*' diff --git a/.github/workflows/tour_of_beam_backend_integration.yml b/.github/workflows/tour_of_beam_backend_integration.yml index f584c7f7e00f..473088150840 100644 --- a/.github/workflows/tour_of_beam_backend_integration.yml +++ b/.github/workflows/tour_of_beam_backend_integration.yml @@ -23,6 +23,7 @@ on: push: branches: ['master', 'release-*'] tags: 'v*' + paths: ['learning/tour-of-beam/backend/**'] pull_request: branches: ['master', 'release-*'] tags: 'v*' @@ -73,8 +74,8 @@ jobs: # 2. start function-framework processes in BG - name: Compile CF run: go build -o ./tob_function cmd/main.go - - name: Run sdkList in background - run: PORT=${{ env.PORT_SDK_LIST }} FUNCTION_TARGET=sdkList ./tob_function & + - name: Run getSdkList in background + run: PORT=${{ env.PORT_SDK_LIST }} FUNCTION_TARGET=getSdkList ./tob_function & - name: Run getContentTree in background run: PORT=${{ env.PORT_GET_CONTENT_TREE }} FUNCTION_TARGET=getContentTree ./tob_function & - name: Run getUnitContent in background diff --git a/learning/tour-of-beam/backend/README.md b/learning/tour-of-beam/backend/README.md index 2be311203b2c..f3e5a0e718d2 100644 --- a/learning/tour-of-beam/backend/README.md +++ b/learning/tour-of-beam/backend/README.md @@ -19,8 +19,8 @@ and currently logged-in user's snippets and progress. Currently it supports Java, Python, and Go Beam SDK. It is comprised of several Cloud Functions, with Firerstore in Datastore mode as a storage. -* list-sdks -* get-content-tree?sdk=(Java|Go|Python) +* get-sdk-list +* get-content-tree?sdk=(java|go|python) * get-unit-content?unitId= TODO: add response schemas TODO: add save functions info diff --git a/learning/tour-of-beam/backend/docker-compose.yml b/learning/tour-of-beam/backend/docker-compose.yml index 5205903791a5..67a289f1ac3f 100644 --- a/learning/tour-of-beam/backend/docker-compose.yml +++ b/learning/tour-of-beam/backend/docker-compose.yml @@ -25,4 +25,4 @@ services: - DATASTORE_LISTEN_ADDRESS=0.0.0.0:8081 ports: - "8081:8081" - command: --consistency=1.0 + command: --consistency=1.0 --store-on-disk diff --git a/learning/tour-of-beam/backend/function.go b/learning/tour-of-beam/backend/function.go index 126c00b5cbd3..363c1585b928 100644 --- a/learning/tour-of-beam/backend/function.go +++ b/learning/tour-of-beam/backend/function.go @@ -20,7 +20,6 @@ package tob import ( "context" "encoding/json" - "fmt" "log" "net/http" "os" @@ -38,54 +37,6 @@ const ( NOT_FOUND = "NOT_FOUND" ) -// Middleware-maker for setting a header -// We also make this less generic: it works with HandlerFunc's -// so that to be convertible to func(w http ResponseWriter, r *http.Request) -// and be accepted by functions.HTTP. -func AddHeader(header, value string) func(http.HandlerFunc) http.HandlerFunc { - return func(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - w.Header().Add(header, value) - next(w, r) - } - } -} - -// Middleware to check http method. -func EnsureMethod(method string) func(http.HandlerFunc) http.HandlerFunc { - return func(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if r.Method == method { - next(w, r) - } else { - w.WriteHeader(http.StatusMethodNotAllowed) - } - } - } -} - -// HandleFunc enriched with sdk. -type HandlerFuncWithSdk func(w http.ResponseWriter, r *http.Request, sdk tob.Sdk) - -// middleware to parse sdk query param and pass it as additional handler param. -func ParseSdkParam(next HandlerFuncWithSdk) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - sdkStr := r.URL.Query().Get("sdk") - sdk := tob.ParseSdk(sdkStr) - - if sdk == tob.SDK_UNDEFINED { - log.Printf("Bad sdk: %v", sdkStr) - - message := fmt.Sprintf("Sdk not in: %v", tob.SdksList()) - finalizeErrResponse(w, http.StatusBadRequest, BAD_FORMAT, message) - - return - } - - next(w, r, sdk) - } -} - // Helper to format http error messages. func finalizeErrResponse(w http.ResponseWriter, status int, code, message string) { resp := tob.CodeMessage{Code: code, Message: message} @@ -115,19 +66,23 @@ func init() { svc = &service.Svc{Repo: &storage.DatastoreDb{Client: client}} } - addHeader := AddHeader("Content-Type", "application/json") - ensureGet := EnsureMethod(http.MethodGet) - // functions framework - functions.HTTP("sdkList", ensureGet(addHeader(sdkList))) - functions.HTTP("getContentTree", ensureGet(addHeader(ParseSdkParam(getContentTree)))) - functions.HTTP("getUnitContent", ensureGet(addHeader(ParseSdkParam(getUnitContent)))) + functions.HTTP("getSdkList", Common(getSdkList)) + functions.HTTP("getContentTree", Common(ParseSdkParam(getContentTree))) + functions.HTTP("getUnitContent", Common(ParseSdkParam(getUnitContent))) } // Get list of SDK names // Used in both representation and accessing content. -func sdkList(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, `{"names": ["Java", "Python", "Go"]}`) +func getSdkList(w http.ResponseWriter, r *http.Request) { + sdks := tob.MakeSdkList() + + err := json.NewEncoder(w).Encode(sdks) + if err != nil { + log.Println("Format sdk list error:", err) + finalizeErrResponse(w, http.StatusInternalServerError, INTERNAL_ERROR, "format sdk list") + return + } } // Get the content tree for a given SDK and user @@ -155,7 +110,7 @@ func getContentTree(w http.ResponseWriter, r *http.Request, sdk tob.Sdk) { // description, hints, code snippets // Required to be wrapped into ParseSdkParam middleware. func getUnitContent(w http.ResponseWriter, r *http.Request, sdk tob.Sdk) { - unitId := r.URL.Query().Get("unitId") + unitId := r.URL.Query().Get("id") unit, err := svc.GetUnitContent(r.Context(), sdk, unitId, nil /*TODO userId*/) if err == service.ErrNoUnit { diff --git a/learning/tour-of-beam/backend/integration_tests/api.go b/learning/tour-of-beam/backend/integration_tests/api.go index f8bb8b38aef1..4bb4f6743656 100644 --- a/learning/tour-of-beam/backend/integration_tests/api.go +++ b/learning/tour-of-beam/backend/integration_tests/api.go @@ -16,13 +16,18 @@ package main // * No hidden fields // * Internal enumerations: sdk, node.type to string params -type sdkListResponse struct { - Names []string +type SdkItem struct { + Id string `json:"id"` + Title string `json:"title"` +} + +type SdkList struct { + Sdks []SdkItem `json:"sdks"` } type Unit struct { - Id string `json:"unitId"` - Name string `json:"name"` + Id string `json:"id"` + Title string `json:"title"` // optional Description string `json:"description,omitempty"` @@ -36,7 +41,7 @@ type Unit struct { } type Group struct { - Name string `json:"name"` + Title string `json:"title"` Nodes []Node `json:"nodes"` } @@ -47,14 +52,14 @@ type Node struct { } type Module struct { - Id string `json:"moduleId"` - Name string `json:"name"` + Id string `json:"id"` + Title string `json:"title"` Complexity string `json:"complexity"` Nodes []Node `json:"nodes"` } type ContentTree struct { - Sdk string `json:"sdk"` + Sdk string `json:"sdkId"` Modules []Module `json:"modules"` } diff --git a/learning/tour-of-beam/backend/integration_tests/client.go b/learning/tour-of-beam/backend/integration_tests/client.go index c66ab779527e..5d43f454d495 100644 --- a/learning/tour-of-beam/backend/integration_tests/client.go +++ b/learning/tour-of-beam/backend/integration_tests/client.go @@ -14,13 +14,31 @@ package main import ( "encoding/json" + "fmt" "io" "net/http" "os" ) -func SdkList(url string) (sdkListResponse, error) { - var result sdkListResponse +var ( + ExpectedHeaders = map[string]string{ + "Access-Control-Allow-Origin": "*", + "Content-Type": "application/json", + } +) + +func verifyHeaders(header http.Header) error { + for k, v := range ExpectedHeaders { + if actual := header.Get(k); actual != v { + return fmt.Errorf("header %s mismatch: %s (expected %s)", k, actual, v) + } + } + + return nil +} + +func GetSdkList(url string) (SdkList, error) { + var result SdkList err := Get(&result, url, nil) return result, err } @@ -33,7 +51,7 @@ func GetContentTree(url, sdk string) (ContentTree, error) { func GetUnitContent(url, sdk, unitId string) (Unit, error) { var result Unit - err := Get(&result, url, map[string]string{"sdk": sdk, "unitId": unitId}) + err := Get(&result, url, map[string]string{"sdk": sdk, "id": unitId}) return result, err } @@ -62,6 +80,10 @@ func Get(dst interface{}, url string, queryParams map[string]string) error { defer resp.Body.Close() + if err := verifyHeaders(resp.Header); err != nil { + return err + } + tee := io.TeeReader(resp.Body, os.Stdout) return json.NewDecoder(tee).Decode(dst) } diff --git a/learning/tour-of-beam/backend/integration_tests/function_test.go b/learning/tour-of-beam/backend/integration_tests/function_test.go index 92ced75f5e7d..06ed66d2a7e3 100644 --- a/learning/tour-of-beam/backend/integration_tests/function_test.go +++ b/learning/tour-of-beam/backend/integration_tests/function_test.go @@ -57,11 +57,14 @@ func TestSdkList(t *testing.T) { t.Fatal(PORT_SDK_LIST, "env not set") } url := "http://localhost:" + port - exp := sdkListResponse{ - Names: []string{"Java", "Python", "Go"}, + + mock_path := filepath.Join("..", "samples", "api", "get_sdk_list.json") + var exp SdkList + if err := loadJson(mock_path, &exp); err != nil { + t.Fatal(err) } - resp, err := SdkList(url) + resp, err := GetSdkList(url) if err != nil { t.Fatal(err) } @@ -81,7 +84,7 @@ func TestGetContentTree(t *testing.T) { t.Fatal(err) } - resp, err := GetContentTree(url, "Python") + resp, err := GetContentTree(url, "python") if err != nil { t.Fatal(err) } @@ -101,7 +104,7 @@ func TestGetUnitContent(t *testing.T) { t.Fatal(err) } - resp, err := GetUnitContent(url, "Python", "challenge1") + resp, err := GetUnitContent(url, "python", "challenge1") if err != nil { t.Fatal(err) } @@ -117,14 +120,14 @@ func TestNegative(t *testing.T) { {PORT_GET_CONTENT_TREE, nil, ErrorResponse{ Code: "BAD_FORMAT", - Message: "Sdk not in: [Java Python Go SCIO]", + Message: "unknown sdk", }, }, - {PORT_GET_CONTENT_TREE, map[string]string{"sdk": "SCIO"}, + {PORT_GET_CONTENT_TREE, map[string]string{"sdk": "scio"}, // TODO: actually here should be a NOT_FOUND error ErrorResponse{Code: "INTERNAL_ERROR", Message: "storage error"}, }, - {PORT_GET_UNIT_CONTENT, map[string]string{"sdk": "Python", "unitId": "unknown_unitId"}, + {PORT_GET_UNIT_CONTENT, map[string]string{"sdk": "python", "unitId": "unknown_unitId"}, ErrorResponse{ Code: "NOT_FOUND", Message: "unit not found", diff --git a/learning/tour-of-beam/backend/integration_tests/local.sh b/learning/tour-of-beam/backend/integration_tests/local.sh index c19c3fb97c8b..6ebebd20f3e3 100644 --- a/learning/tour-of-beam/backend/integration_tests/local.sh +++ b/learning/tour-of-beam/backend/integration_tests/local.sh @@ -27,7 +27,7 @@ docker-compose up -d go build -o tob_function cmd/main.go -PORT=$PORT_SDK_LIST FUNCTION_TARGET=sdkList ./tob_function & +PORT=$PORT_SDK_LIST FUNCTION_TARGET=getSdkList ./tob_function & PORT=$PORT_GET_CONTENT_TREE FUNCTION_TARGET=getContentTree ./tob_function & PORT=$PORT_GET_UNIT_CONTENT FUNCTION_TARGET=getUnitContent ./tob_function & @@ -49,7 +49,7 @@ docker-compose down ls "$DATASTORE_EMULATOR_DATADIR" cat "$DATASTORE_EMULATOR_DATADIR/WEB-INF/index.yaml" -diff -q "$DATASTORE_EMULATOR_DATADIR/WEB-INF/index.yaml" internal/storage/index.yaml || ( echo "index.yaml mismatch"; exit 1) +diff "$DATASTORE_EMULATOR_DATADIR/WEB-INF/index.yaml" internal/storage/index.yaml || ( echo "index.yaml mismatch"; exit 1) rm -rf "$DATASTORE_EMULATOR_DATADIR" diff --git a/learning/tour-of-beam/backend/internal/entity.go b/learning/tour-of-beam/backend/internal/entity.go index 22c48f77ee95..55ee75f96e64 100644 --- a/learning/tour-of-beam/backend/internal/entity.go +++ b/learning/tour-of-beam/backend/internal/entity.go @@ -15,9 +15,18 @@ package internal +type SdkItem struct { + Id string `json:"id"` + Title string `json:"title"` +} + +type SdkList struct { + Sdks []SdkItem `json:"sdks"` +} + type Unit struct { - Id string `json:"unitId"` - Name string `json:"name"` + Id string `json:"id"` + Title string `json:"title"` // optional Description string `json:"description,omitempty"` @@ -41,7 +50,7 @@ const ( ) type Group struct { - Name string `json:"name"` + Title string `json:"title"` Nodes []Node `json:"nodes"` } @@ -52,14 +61,14 @@ type Node struct { } type Module struct { - Id string `json:"moduleId"` - Name string `json:"name"` + Id string `json:"id"` + Title string `json:"title"` Complexity string `json:"complexity"` Nodes []Node `json:"nodes"` } type ContentTree struct { - Sdk Sdk `json:"sdk"` + Sdk Sdk `json:"sdkId"` Modules []Module `json:"modules"` } diff --git a/learning/tour-of-beam/backend/internal/fs_content/builders.go b/learning/tour-of-beam/backend/internal/fs_content/builders.go index 84895431cb08..715cf444b493 100644 --- a/learning/tour-of-beam/backend/internal/fs_content/builders.go +++ b/learning/tour-of-beam/backend/internal/fs_content/builders.go @@ -26,7 +26,7 @@ type UnitBuilder struct { func NewUnitBuilder(info learningUnitInfo) UnitBuilder { return UnitBuilder{tob.Unit{ Id: info.Id, - Name: info.Name, + Title: info.Name, TaskName: info.TaskName, SolutionName: info.SolutionName, }} diff --git a/learning/tour-of-beam/backend/internal/fs_content/load.go b/learning/tour-of-beam/backend/internal/fs_content/load.go index 8c9ea4271045..37112ceb8ac1 100644 --- a/learning/tour-of-beam/backend/internal/fs_content/load.go +++ b/learning/tour-of-beam/backend/internal/fs_content/load.go @@ -115,7 +115,7 @@ func collectUnit(infopath string, ids_watcher *idsWatcher) (unit *tob.Unit, err func collectGroup(infopath string, ids_watcher *idsWatcher) (*tob.Group, error) { info := loadLearningGroupInfo(infopath) log.Printf("Found Group %v metadata at %v\n", info.Name, infopath) - group := tob.Group{Name: info.Name} + group := tob.Group{Title: info.Name} for _, item := range info.Content { node, err := collectNode(filepath.Join(infopath, "..", item), ids_watcher) if err != nil { @@ -153,7 +153,7 @@ func collectModule(infopath string, ids_watcher *idsWatcher) (tob.Module, error) info := loadLearningModuleInfo(infopath) log.Printf("Found Module %v metadata at %v\n", info.Id, infopath) ids_watcher.CheckId(info.Id) - module := tob.Module{Id: info.Id, Name: info.Name, Complexity: info.Complexity} + module := tob.Module{Id: info.Id, Title: info.Name, Complexity: info.Complexity} for _, item := range info.Content { node, err := collectNode(filepath.Join(infopath, "..", item), ids_watcher) if err != nil { diff --git a/learning/tour-of-beam/backend/internal/fs_content/load_test.go b/learning/tour-of-beam/backend/internal/fs_content/load_test.go index b5c50424dc76..d5478a64d64c 100644 --- a/learning/tour-of-beam/backend/internal/fs_content/load_test.go +++ b/learning/tour-of-beam/backend/internal/fs_content/load_test.go @@ -25,7 +25,7 @@ import ( func genUnitNode(id string) tob.Node { return tob.Node{Type: tob.NODE_UNIT, Unit: &tob.Unit{ - Id: id, Name: "Challenge Name", + Id: id, Title: "Challenge Name", Description: "## Challenge description\n\nawesome description\n", Hints: []string{ "## Hint 1\n\nhint 1", @@ -42,16 +42,16 @@ func TestSample(t *testing.T) { Sdk: tob.SDK_JAVA, Modules: []tob.Module{ { - Id: "module1", Name: "Module One", Complexity: "BASIC", + Id: "module1", Title: "Module One", Complexity: "BASIC", Nodes: []tob.Node{ - {Type: tob.NODE_UNIT, Unit: &tob.Unit{Id: "example1", Name: "Example Unit Name"}}, + {Type: tob.NODE_UNIT, Unit: &tob.Unit{Id: "example1", Title: "Example Unit Name"}}, genUnitNode("challenge1"), }, }, { - Id: "module2", Name: "Module Two", Complexity: "MEDIUM", + Id: "module2", Title: "Module Two", Complexity: "MEDIUM", Nodes: []tob.Node{ - {Type: tob.NODE_UNIT, Unit: &tob.Unit{Id: "example21", Name: "Example Unit Name"}}, + {Type: tob.NODE_UNIT, Unit: &tob.Unit{Id: "example21", Title: "Example Unit Name"}}, genUnitNode("challenge21"), }, }, @@ -61,13 +61,13 @@ func TestSample(t *testing.T) { Sdk: tob.SDK_PYTHON, Modules: []tob.Module{ { - Id: "module1", Name: "Module One", Complexity: "BASIC", + Id: "module1", Title: "Module One", Complexity: "BASIC", Nodes: []tob.Node{ - {Type: tob.NODE_UNIT, Unit: &tob.Unit{Id: "intro-unit", Name: "Intro Unit Name"}}, + {Type: tob.NODE_UNIT, Unit: &tob.Unit{Id: "intro-unit", Title: "Intro Unit Name"}}, { Type: tob.NODE_GROUP, Group: &tob.Group{ - Name: "The Group", Nodes: []tob.Node{ - {Type: tob.NODE_UNIT, Unit: &tob.Unit{Id: "example1", Name: "Example Unit Name"}}, + Title: "The Group", Nodes: []tob.Node{ + {Type: tob.NODE_UNIT, Unit: &tob.Unit{Id: "example1", Title: "Example Unit Name"}}, genUnitNode("challenge1"), }, }, diff --git a/learning/tour-of-beam/backend/internal/sdk.go b/learning/tour-of-beam/backend/internal/sdk.go index a1451d183755..1888481def7d 100644 --- a/learning/tour-of-beam/backend/internal/sdk.go +++ b/learning/tour-of-beam/backend/internal/sdk.go @@ -19,33 +19,54 @@ type Sdk string const ( SDK_UNDEFINED Sdk = "" - SDK_GO Sdk = "Go" - SDK_PYTHON Sdk = "Python" - SDK_JAVA Sdk = "Java" - SDK_SCIO Sdk = "SCIO" + SDK_GO Sdk = "go" + SDK_PYTHON Sdk = "python" + SDK_JAVA Sdk = "java" + SDK_SCIO Sdk = "scio" ) func (s Sdk) String() string { return string(s) } -// Parse sdk from string names, f.e. "Java" -> Sdk.GO_JAVA +// get Title which is shown on the landing page +func (s Sdk) Title() string { + switch s { + case SDK_GO: + return "Go" + case SDK_JAVA: + return "Java" + case SDK_PYTHON: + return "Python" + case SDK_SCIO: + return "SCIO" + default: + panic("undefined/unknown SDK title") + } +} + +// Parse sdk from string names, f.e. "java" -> Sdk.GO_JAVA +// Make allowance for the case if the Title is given, not Id // Returns SDK_UNDEFINED on error. func ParseSdk(s string) Sdk { switch s { - case "Go": + case "go", "Go": return SDK_GO - case "Python": + case "python", "Python": return SDK_PYTHON - case "Java": + case "java", "Java": return SDK_JAVA - case "SCIO": + case "scio", "SCIO": return SDK_SCIO default: return SDK_UNDEFINED } } -func SdksList() [4]string { - return [4]string{"Java", "Python", "Go", "SCIO"} +func MakeSdkList() SdkList { + sdks := make([]SdkItem, 0, 4) + for _, sdk := range []Sdk{SDK_JAVA, SDK_PYTHON, SDK_GO, SDK_SCIO} { + sdks = append(sdks, SdkItem{Id: sdk.String(), Title: sdk.Title()}) + } + return SdkList{Sdks: sdks} } diff --git a/learning/tour-of-beam/backend/internal/sdk_test.go b/learning/tour-of-beam/backend/internal/sdk_test.go index 562679952c1a..593c082bf1e0 100644 --- a/learning/tour-of-beam/backend/internal/sdk_test.go +++ b/learning/tour-of-beam/backend/internal/sdk_test.go @@ -15,45 +15,61 @@ package internal -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" +) func TestParse(t *testing.T) { for _, s := range []struct { str string expected Sdk }{ + {"go", SDK_GO}, + {"python", SDK_PYTHON}, + {"java", SDK_JAVA}, + {"scio", SDK_SCIO}, + {"Go", SDK_GO}, {"Python", SDK_PYTHON}, {"Java", SDK_JAVA}, {"SCIO", SDK_SCIO}, - {"Bad", SDK_UNDEFINED}, + {"", SDK_UNDEFINED}, } { - if parsed := ParseSdk(s.str); parsed != s.expected { - t.Errorf("Failed to parse %v: got %v (expected %v)", s.str, parsed, s.expected) - } + assert.Equal(t, s.expected, ParseSdk(s.str)) } } func TestSerialize(t *testing.T) { for _, s := range []struct { - expected string - sdk Sdk + expectedId, expectedTitle string + sdk Sdk }{ - {"Go", SDK_GO}, - {"Python", SDK_PYTHON}, - {"Java", SDK_JAVA}, - {"SCIO", SDK_SCIO}, - {"", SDK_UNDEFINED}, + {"go", "Go", SDK_GO}, + {"python", "Python", SDK_PYTHON}, + {"java", "Java", SDK_JAVA}, + {"scio", "SCIO", SDK_SCIO}, + {"", "", SDK_UNDEFINED}, } { - if txt := s.sdk.String(); txt != s.expected { - t.Errorf("Failed to serialize %v to string: got %v (expected %v)", s.sdk, txt, s.expected) + assert.Equal(t, s.expectedId, s.sdk.String()) + if s.sdk == SDK_UNDEFINED { + assert.Panics(t, func() { s.sdk.Title() }) + } else { + assert.Equal(t, s.expectedTitle, s.sdk.Title()) } } } func TestSdkList(t *testing.T) { - if SdksList() != [4]string{"Java", "Python", "Go", "SCIO"} { - t.Error("Sdk list mismatch: ", SdksList()) - } + + assert.Equal(t, SdkList{ + []SdkItem{ + {"java", "Java"}, + {"python", "Python"}, + {"go", "Go"}, + {"scio", "SCIO"}, + }, + }, MakeSdkList()) } diff --git a/learning/tour-of-beam/backend/internal/storage/adapter.go b/learning/tour-of-beam/backend/internal/storage/adapter.go index ac7a1c2b25f3..55a6bfb69411 100644 --- a/learning/tour-of-beam/backend/internal/storage/adapter.go +++ b/learning/tour-of-beam/backend/internal/storage/adapter.go @@ -54,16 +54,16 @@ func MakeUnitNode(unit *tob.Unit, order, level int) *TbLearningNode { return nil } return &TbLearningNode{ - Id: unit.Id, - Name: unit.Name, + Id: unit.Id, + Title: unit.Title, Type: tob.NODE_UNIT, Order: order, Level: level, Unit: &TbLearningUnit{ - Id: unit.Id, - Name: unit.Name, + Id: unit.Id, + Title: unit.Title, Description: unit.Description, Hints: unit.Hints, @@ -80,28 +80,28 @@ func MakeGroupNode(group *tob.Group, order, level int) *TbLearningNode { return &TbLearningNode{ // ID doesn't make much sense for groups, // but we have to define it to include in queries - Id: group.Name, - Name: group.Name, + Id: group.Title, + Title: group.Title, Type: tob.NODE_GROUP, Order: order, Level: level, Group: &TbLearningGroup{ - Name: group.Name, + Title: group.Title, }, } } // Depending on the projection, we either convert TbLearningUnit to a model -// Or we use common fields Id, Name to make it. -func FromDatastoreUnit(tbUnit *TbLearningUnit, id, name string) *tob.Unit { +// Or we use common fields Id, Title to make it. +func FromDatastoreUnit(tbUnit *TbLearningUnit, id, title string) *tob.Unit { if tbUnit == nil { - return &tob.Unit{Id: id, Name: name} + return &tob.Unit{Id: id, Title: title} } return &tob.Unit{ Id: tbUnit.Id, - Name: tbUnit.Name, + Title: tbUnit.Title, Description: tbUnit.Description, Hints: tbUnit.Hints, TaskSnippetId: tbUnit.TaskSnippetId, @@ -110,13 +110,13 @@ func FromDatastoreUnit(tbUnit *TbLearningUnit, id, name string) *tob.Unit { } // Depending on the projection, we either convert TbLearningGroup to a model -// Or we use common field Name to make it. -func FromDatastoreGroup(tbGroup *TbLearningGroup, name string) *tob.Group { +// Or we use common field Title to make it. +func FromDatastoreGroup(tbGroup *TbLearningGroup, title string) *tob.Group { if tbGroup == nil { - return &tob.Group{Name: name} + return &tob.Group{Title: title} } return &tob.Group{ - Name: tbGroup.Name, + Title: tbGroup.Title, } } @@ -126,9 +126,9 @@ func FromDatastoreNode(tbNode TbLearningNode) tob.Node { } switch tbNode.Type { case tob.NODE_GROUP: - node.Group = FromDatastoreGroup(tbNode.Group, tbNode.Name) + node.Group = FromDatastoreGroup(tbNode.Group, tbNode.Title) case tob.NODE_UNIT: - node.Unit = FromDatastoreUnit(tbNode.Unit, tbNode.Id, tbNode.Name) + node.Unit = FromDatastoreUnit(tbNode.Unit, tbNode.Id, tbNode.Title) default: panic("undefined node type") } @@ -138,7 +138,7 @@ func FromDatastoreNode(tbNode TbLearningNode) tob.Node { func MakeDatastoreModule(mod *tob.Module, order int) *TbLearningModule { return &TbLearningModule{ Id: mod.Id, - Name: mod.Name, + Title: mod.Title, Complexity: mod.Complexity, Order: order, diff --git a/learning/tour-of-beam/backend/internal/storage/datastore.go b/learning/tour-of-beam/backend/internal/storage/datastore.go index 62c55b2ba5b2..24a12bb4dbe4 100644 --- a/learning/tour-of-beam/backend/internal/storage/datastore.go +++ b/learning/tour-of-beam/backend/internal/storage/datastore.go @@ -47,7 +47,7 @@ func (d *DatastoreDb) collectModules(ctx context.Context, tx *datastore.Transact } for _, tbMod := range tbMods { - mod := tob.Module{Id: tbMod.Id, Name: tbMod.Name, Complexity: tbMod.Complexity} + mod := tob.Module{Id: tbMod.Id, Title: tbMod.Title, Complexity: tbMod.Complexity} mod.Nodes, err = d.collectNodes(ctx, tx, tbMod.Key, 0) if err != nil { return modules, err @@ -72,7 +72,7 @@ func (d *DatastoreDb) collectNodes(ctx context.Context, tx *datastore.Transactio Namespace(PgNamespace). Ancestor(parentKey). FilterField("level", "=", level). - Project("type", "id", "name"). + Project("type", "id", "title"). Order("order"). Transaction(tx) if _, err = d.Client.GetAll(ctx, queryNodes, &tbNodes); err != nil { @@ -189,7 +189,7 @@ func (d *DatastoreDb) saveContentTree(tx *datastore.Transaction, tree *tob.Conte } rootKey := pgNameKey(TbLearningPathKind, sdkToKey(tree.Sdk), nil) - tbLP := TbLearningPath{Name: tree.Sdk.String()} + tbLP := TbLearningPath{Title: tree.Sdk.String()} if _, err := tx.Put(rootKey, &tbLP); err != nil { return fmt.Errorf("failed to put learning_path: %w", err) } diff --git a/learning/tour-of-beam/backend/internal/storage/index.yaml b/learning/tour-of-beam/backend/internal/storage/index.yaml index fa78d72f9481..65658f14a6ea 100644 --- a/learning/tour-of-beam/backend/internal/storage/index.yaml +++ b/learning/tour-of-beam/backend/internal/storage/index.yaml @@ -16,5 +16,5 @@ indexes: - name: "level" - name: "order" - name: "id" - - name: "name" + - name: "title" - name: "type" diff --git a/learning/tour-of-beam/backend/internal/storage/schema.go b/learning/tour-of-beam/backend/internal/storage/schema.go index 8550d4aed695..1ed249dd0a3c 100644 --- a/learning/tour-of-beam/backend/internal/storage/schema.go +++ b/learning/tour-of-beam/backend/internal/storage/schema.go @@ -40,15 +40,15 @@ const ( // tb_learning_path. type TbLearningPath struct { - Key *datastore.Key `datastore:"__key__"` - Name string `datastore:"name"` + Key *datastore.Key `datastore:"__key__"` + Title string `datastore:"title"` } // tb_learning_module. type TbLearningModule struct { Key *datastore.Key `datastore:"__key__"` Id string `datastore:"id"` - Name string `datastore:"name"` + Title string `datastore:"title"` Complexity string `datastore:"complexity"` // internal, only db @@ -57,14 +57,14 @@ type TbLearningModule struct { // tb_learning_node.group. type TbLearningGroup struct { - Name string `datastore:"name"` + Title string `datastore:"title"` } // tb_learning_node.unit // Learning Unit content. type TbLearningUnit struct { Id string `datastore:"id"` - Name string `datastore:"name"` + Title string `datastore:"title"` Description string `datastore:"description,noindex"` Hints []string `datastore:"hints,noindex"` @@ -78,8 +78,8 @@ type TbLearningNode struct { Type tob.NodeType `datastore:"type"` // common fields, duplicate same fields from the nested entities // (needed to allow projection when getting the content tree) - Id string `datastore:"id"` - Name string `datastore:"name"` + Id string `datastore:"id"` + Title string `datastore:"title"` // type-specific nested info Unit *TbLearningUnit `datastore:"unit,noindex"` diff --git a/learning/tour-of-beam/backend/middleware.go b/learning/tour-of-beam/backend/middleware.go new file mode 100644 index 000000000000..87c98bd6e14d --- /dev/null +++ b/learning/tour-of-beam/backend/middleware.go @@ -0,0 +1,79 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package tob + +import ( + "log" + "net/http" + + tob "beam.apache.org/learning/tour-of-beam/backend/internal" +) + +// Middleware-maker for setting a header +// We also make this less generic: it works with HandlerFunc's +// so that to be convertible to func(w http ResponseWriter, r *http.Request) +// and be accepted by functions.HTTP. +func AddHeader(header, value string) func(http.HandlerFunc) http.HandlerFunc { + return func(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Add(header, value) + next(w, r) + } + } +} + +// Middleware to check http method. +func EnsureMethod(method string) func(http.HandlerFunc) http.HandlerFunc { + return func(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method == method { + next(w, r) + } else { + w.WriteHeader(http.StatusMethodNotAllowed) + } + } + } +} + +// Helper common AIO middleware +func Common(next http.HandlerFunc) http.HandlerFunc { + addContentType := AddHeader("Content-Type", "application/json") + addCORS := AddHeader("Access-Control-Allow-Origin", "*") + ensureGet := EnsureMethod(http.MethodGet) + + return ensureGet(addCORS(addContentType(next))) +} + +// HandleFunc enriched with sdk. +type HandlerFuncWithSdk func(w http.ResponseWriter, r *http.Request, sdk tob.Sdk) + +// middleware to parse sdk query param and pass it as additional handler param. +func ParseSdkParam(next HandlerFuncWithSdk) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + sdkStr := r.URL.Query().Get("sdk") + sdk := tob.ParseSdk(sdkStr) + + if sdk == tob.SDK_UNDEFINED { + log.Printf("Bad sdk: %v", sdkStr) + finalizeErrResponse(w, http.StatusBadRequest, BAD_FORMAT, "unknown sdk") + return + } + + next(w, r, sdk) + } +} diff --git a/learning/tour-of-beam/backend/samples/api/get_content_tree.json b/learning/tour-of-beam/backend/samples/api/get_content_tree.json index cf8c40b6f73b..1c0a208a55e3 100644 --- a/learning/tour-of-beam/backend/samples/api/get_content_tree.json +++ b/learning/tour-of-beam/backend/samples/api/get_content_tree.json @@ -2,32 +2,32 @@ "modules" : [ { "complexity" : "BASIC", - "moduleId" : "module1", - "name" : "Module One", + "id" : "module1", + "title" : "Module One", "nodes" : [ { "type" : "unit", "unit" : { - "name" : "Intro Unit Name", - "unitId" : "intro-unit" + "title" : "Intro Unit Name", + "id" : "intro-unit" } }, { "group" : { - "name" : "The Group", + "title" : "The Group", "nodes" : [ { "type" : "unit", "unit" : { - "name" : "Example Unit Name", - "unitId" : "example1" + "title" : "Example Unit Name", + "id" : "example1" } }, { "type" : "unit", "unit" : { - "name" : "Challenge Name", - "unitId" : "challenge1" + "title" : "Challenge Name", + "id" : "challenge1" } } ] @@ -37,5 +37,5 @@ ] } ], - "sdk" : "Python" + "sdkId" : "python" } \ No newline at end of file diff --git a/learning/tour-of-beam/backend/samples/api/get_sdk_list.json b/learning/tour-of-beam/backend/samples/api/get_sdk_list.json new file mode 100644 index 000000000000..b24d25f1f9ed --- /dev/null +++ b/learning/tour-of-beam/backend/samples/api/get_sdk_list.json @@ -0,0 +1,8 @@ +{ + "sdks" : [ + {"id": "java", "title": "Java"}, + {"id": "python", "title": "Python"}, + {"id": "go", "title": "Go"}, + {"id": "scio", "title": "SCIO"} + ] +} \ No newline at end of file diff --git a/learning/tour-of-beam/backend/samples/api/get_unit_content.json b/learning/tour-of-beam/backend/samples/api/get_unit_content.json index 82337277ff0f..a337eaf52efa 100644 --- a/learning/tour-of-beam/backend/samples/api/get_unit_content.json +++ b/learning/tour-of-beam/backend/samples/api/get_unit_content.json @@ -1,6 +1,6 @@ { - "unitId": "challenge1", - "name": "Challenge Name", + "id": "challenge1", + "title": "Challenge Name", "description": "## Challenge description\n\nawesome description\n", "hints" : [ "## Hint 1\n\nhint 1", diff --git a/learning/tour-of-beam/backend/samples/api/get_unit_content_full.json b/learning/tour-of-beam/backend/samples/api/get_unit_content_full.json index 3fc1bc26e53a..573f4f09ea05 100644 --- a/learning/tour-of-beam/backend/samples/api/get_unit_content_full.json +++ b/learning/tour-of-beam/backend/samples/api/get_unit_content_full.json @@ -1,6 +1,6 @@ { - "unitId": "challenge1", - "name": "Challenge Name", + "id": "challenge1", + "title": "Challenge Name", "description": "## Challenge description\n\nawesome description\n", "hints" : [ "## Hint 1\n\nhint 1",