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",