You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@beam.apache.org by da...@apache.org on 2022/09/28 16:37:52 UTC
[beam] branch master updated: [Tour Of Beam] API adjustments (#23349)
This is an automated email from the ASF dual-hosted git repository.
damccorm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git
The following commit(s) were added to refs/heads/master by this push:
new a393efa61d4 [Tour Of Beam] API adjustments (#23349)
a393efa61d4 is described below
commit a393efa61d4a1933717a4fe24a7559ec00e8912a
Author: Evgeny Antyshev <ea...@gmail.com>
AuthorDate: Wed Sep 28 19:37:43 2022 +0300
[Tour Of Beam] API adjustments (#23349)
* sdk
* use sample/api
* sdk_list.json
* nits
* title
* fix integration_tests
* unit/module id
* sdks
* unitId->id in param
* id/title fix
* empty
* CORS
* optimize
* Update sdk.go
* fixing format error
Co-authored-by: oborysevych <ol...@akvelon.com>
---
.github/workflows/tour_of_beam_backend.yml | 1 +
.../workflows/tour_of_beam_backend_integration.yml | 5 +-
learning/tour-of-beam/backend/README.md | 4 +-
learning/tour-of-beam/backend/docker-compose.yml | 2 +-
learning/tour-of-beam/backend/function.go | 71 ++++---------------
.../tour-of-beam/backend/integration_tests/api.go | 21 +++---
.../backend/integration_tests/client.go | 28 +++++++-
.../backend/integration_tests/function_test.go | 19 +++---
.../backend/integration_tests/local.sh | 4 +-
learning/tour-of-beam/backend/internal/entity.go | 21 ++++--
.../backend/internal/fs_content/builders.go | 2 +-
.../backend/internal/fs_content/load.go | 4 +-
.../backend/internal/fs_content/load_test.go | 18 ++---
learning/tour-of-beam/backend/internal/sdk.go | 43 +++++++++---
learning/tour-of-beam/backend/internal/sdk_test.go | 50 +++++++++-----
.../backend/internal/storage/adapter.go | 36 +++++-----
.../backend/internal/storage/datastore.go | 6 +-
.../backend/internal/storage/index.yaml | 2 +-
.../backend/internal/storage/schema.go | 14 ++--
learning/tour-of-beam/backend/middleware.go | 79 ++++++++++++++++++++++
.../backend/samples/api/get_content_tree.json | 20 +++---
.../backend/samples/api/get_sdk_list.json | 8 +++
.../backend/samples/api/get_unit_content.json | 4 +-
.../backend/samples/api/get_unit_content_full.json | 4 +-
24 files changed, 293 insertions(+), 173 deletions(-)
diff --git a/.github/workflows/tour_of_beam_backend.yml b/.github/workflows/tour_of_beam_backend.yml
index 7182911b3c2..c87f962cc39 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 f584c7f7e00..47308815084 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 2be311203b2..f3e5a0e718d 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=<id>
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 5205903791a..67a289f1ac3 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 126c00b5cbd..363c1585b92 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 f8bb8b38aef..4bb4f674365 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 c66ab779527..5d43f454d49 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 92ced75f5e7..06ed66d2a7e 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 c19c3fb97c8..6ebebd20f3e 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 22c48f77ee9..55ee75f96e6 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 84895431cb0..715cf444b49 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 8c9ea427104..37112ceb8ac 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 b5c50424dc7..d5478a64d64 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 a1451d18375..1888481def7 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 562679952c1..593c082bf1e 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 ac7a1c2b25f..55a6bfb6941 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 62c55b2ba5b..24a12bb4dbe 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 fa78d72f948..65658f14a6e 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 8550d4aed69..1ed249dd0a3 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 00000000000..87c98bd6e14
--- /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 cf8c40b6f73..1c0a208a55e 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 00000000000..b24d25f1f9e
--- /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 82337277ff0..a337eaf52ef 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 3fc1bc26e53..573f4f09ea0 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",