Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tour of beam learning materials CI/CD refactoring and templating #25080

Merged
153 changes: 124 additions & 29 deletions learning/tour-of-beam/backend/internal/fs_content/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@
package fs_content

import (
"bytes"
"fmt"
"io/fs"
"io/ioutil"
"log"
"os"
"path/filepath"
"regexp"
"text/template"

tob "beam.apache.org/learning/tour-of-beam/backend/internal"
)
Expand All @@ -38,32 +39,45 @@ const (
)

type learningPathInfo struct {
Sdk string `yaml:"sdk"`
Sdk []string `yaml:"sdk"`
Content []string `yaml:"content"`
}

type learningModuleInfo struct {
Sdk []string `yaml:"sdk"`
Id string `yaml:"id"`
Name string `yaml:"name"`
Complexity string `yaml:"complexity"`
Content []string `yaml:"content"`
}

type learningGroupInfo struct {
Sdk []string `yaml:"sdk"`
Id string `yaml:"id"`
Name string `yaml:"name"`
Content []string `yaml:"content"`
}

type learningUnitInfo struct {
Id string `yaml:"id"`
Name string `yaml:"name"`
TaskName string `yaml:"taskName"`
SolutionName string `yaml:"solutionName"`
Sdk []string `yaml:"sdk"`
Id string `yaml:"id"`
Name string `yaml:"name"`
TaskName string `yaml:"taskName"`
SolutionName string `yaml:"solutionName"`
}

func collectUnit(infopath string, ctx *sdkContext) (unit *tob.Unit, err error) {
info := loadLearningUnitInfo(infopath)

supported, err := isSupportedSdk(info.Sdk, ctx, infopath)
if err != nil {
return nil, err
}
if !supported {
log.Printf("Unit %v at %v not supported in %v\n", info.Id, infopath, ctx.sdk)
return nil, nil
}

log.Printf("Found Unit %v metadata at %v\n", info.Id, infopath)
ctx.idsWatcher.CheckId(info.Id)
builder := NewUnitBuilder(info, ctx.sdk)
Expand All @@ -77,15 +91,19 @@ func collectUnit(infopath string, ctx *sdkContext) (unit *tob.Unit, err error) {
return filepath.SkipDir

case d.Name() == descriptionMd:
content, err := ioutil.ReadFile(path)
templateSource, err := os.ReadFile(path)
if err != nil {
return err
}
content, err := processTemplate(templateSource, ctx.sdk)
if err != nil {
return err
}
builder.AddDescription(string(content))

// Here we rely on that WalkDir entries are lexically sorted
case regexp.MustCompile(hintMdRegexp).MatchString(d.Name()):
content, err := ioutil.ReadFile(path)
content, err := os.ReadFile(path)
if err != nil {
return err
}
Expand All @@ -97,27 +115,57 @@ func collectUnit(infopath string, ctx *sdkContext) (unit *tob.Unit, err error) {
return builder.Build(), err
}

func processTemplate(source []byte, sdk tob.Sdk) ([]byte, error) {
t := template.New("")
t, err := t.Parse(string(source))
if err != nil {
return nil, err
}

var output bytes.Buffer
err = t.Execute(&output, struct{ Sdk tob.Sdk }{Sdk: sdk})
if err != nil {
return nil, err
}

return output.Bytes(), nil
}

func collectGroup(infopath string, ctx *sdkContext) (*tob.Group, error) {
info := loadLearningGroupInfo(infopath)

supported, err := isSupportedSdk(info.Sdk, ctx, infopath)
if err != nil {
return nil, err
}
if !supported {
log.Printf("Group %v at %v not supported in %v\n", info.Id, infopath, ctx.sdk)
return nil, nil
}

log.Printf("Found Group %v metadata at %v\n", info.Name, infopath)
group := tob.Group{Id: info.Id, Title: info.Name}
for _, item := range info.Content {
node, err := collectNode(filepath.Join(infopath, "..", item), ctx)
if err != nil {
return &group, err
}
group.Nodes = append(group.Nodes, node)
if node == nil {
continue
}
group.Nodes = append(group.Nodes, *node)
}

return &group, nil
}

// Collect node which is either a unit or a group.
func collectNode(rootpath string, ctx *sdkContext) (node tob.Node, err error) {
func collectNode(rootpath string, ctx *sdkContext) (*tob.Node, error) {
files, err := os.ReadDir(rootpath)
if err != nil {
return node, err
return nil, err
}
node := &tob.Node{}
for _, f := range files {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't technically a problem with this code change, but I think this loop has a potential problem. If the first file has an associated error, but the second does not it will reset err to nil

Probably we should be early returning if err != nil after the switch

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! Nice catch!

switch f.Name() {
case unitInfoYaml:
Expand All @@ -131,42 +179,66 @@ func collectNode(rootpath string, ctx *sdkContext) (node tob.Node, err error) {
if node.Type == tob.NODE_UNDEFINED {
return node, fmt.Errorf("node undefined at %v", rootpath)
}
if node.Group == nil && node.Unit == nil {
return nil, err
}
return node, err
}

func collectModule(infopath string, ctx *sdkContext) (tob.Module, error) {
func collectModule(infopath string, ctx *sdkContext) (*tob.Module, error) {
info := loadLearningModuleInfo(infopath)

supported, err := isSupportedSdk(info.Sdk, ctx, infopath)
if err != nil {
return nil, err
}
if !supported {
log.Printf("Module %v at %v not supported in %v\n", info.Id, infopath, ctx.sdk)
return nil, nil
}

log.Printf("Found Module %v metadata at %v\n", info.Id, infopath)
ctx.idsWatcher.CheckId(info.Id)
module := tob.Module{Id: info.Id, Title: info.Name, Complexity: info.Complexity}
for _, item := range info.Content {
node, err := collectNode(filepath.Join(infopath, "..", item), ctx)
if err != nil {
return tob.Module{}, err
return nil, err
}
if node == nil {
continue
}
module.Nodes = append(module.Nodes, node)
module.Nodes = append(module.Nodes, *node)
}

return module, nil
return &module, nil
}

func collectSdk(infopath string) (tree tob.ContentTree, err error) {
func collectSdk(infopath string) (trees []tob.ContentTree, err error) {
info := loadLearningPathInfo(infopath)
tree.Sdk = tob.ParseSdk(info.Sdk)
if tree.Sdk == tob.SDK_UNDEFINED {
return tree, fmt.Errorf("unknown SDK at %v", infopath)

sdks, err := getSupportedSdk(info.Sdk, infopath)
if err != nil {
return trees, err
}
log.Printf("Found Sdk %v metadata at %v\n", info.Sdk, infopath)
ctx := newSdkContext(tree.Sdk)
for _, item := range info.Content {
mod, err := collectModule(filepath.Join(infopath, "..", item, moduleInfoYaml), ctx)
if err != nil {
return tree, err
for sdk := range sdks {
tree := tob.ContentTree{}
tree.Sdk = sdk
log.Printf("Found Sdk %v metadata at %v\n", sdk, infopath)
ctx := newSdkContext(tree.Sdk)
for _, item := range info.Content {
mod, err := collectModule(filepath.Join(infopath, "..", item, moduleInfoYaml), ctx)
if err != nil {
return trees, err
}
if mod != nil {
tree.Modules = append(tree.Modules, *mod)
}
}
tree.Modules = append(tree.Modules, mod)
trees = append(trees, tree)
}

return tree, nil
return trees, nil
}

// Build a content tree for each SDK
Expand All @@ -179,11 +251,12 @@ func CollectLearningTree(rootpath string) (trees []tob.ContentTree, err error) {
return err
}
if d.Name() == contentInfoYaml {
tree, err := collectSdk(path)

collected, err := collectSdk(path)
if err != nil {
return err
}
trees = append(trees, tree)
trees = append(trees, collected...)
// don't walk into SDK subtree (already done by collectSdk)
return filepath.SkipDir
}
Expand All @@ -192,3 +265,25 @@ func CollectLearningTree(rootpath string) (trees []tob.ContentTree, err error) {

return trees, err
}

func isSupportedSdk(sdks []string, ctx *sdkContext, infopath string) (ok bool, err error) {
sdk, err := getSupportedSdk(sdks, infopath)
if err != nil {
return false, err
}
_, ok = sdk[ctx.sdk]
return
}

func getSupportedSdk(sdk []string, infopath string) (map[tob.Sdk]bool, error) {
sdks := make(map[tob.Sdk]bool)
for _, s := range sdk {
curSdk := tob.ParseSdk(s)
if curSdk == tob.SDK_UNDEFINED {
return sdks, fmt.Errorf("unknown SDK at %v", infopath)
}
sdks[curSdk] = true
}

return sdks, nil
}
68 changes: 68 additions & 0 deletions learning/tour-of-beam/backend/internal/fs_content/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func getExampleNode(id string, sdk tob.Sdk) tob.Node {
}

func TestSample(t *testing.T) {
//trees, err := CollectLearningTree(filepath.Join("..", "..", "samples", "learning-content"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove commented out code

trees, err := CollectLearningTree(filepath.Join("..", "..", "samples", "learning-content"))
assert.Nil(t, err)
assert.Equal(t, 2, len(trees))
Expand Down Expand Up @@ -90,3 +91,70 @@ func TestSample(t *testing.T) {
},
}, trees[1])
}

// TestTemplates test that templating engine is used correctly.
// The test itself is intended as an example of typical template usage.
func TestTemplateProcessing(t *testing.T) {
goSdkExpected := "Go SDK"
pythonSdkExpected := "Python SDK"
javaSdkExpected := "Java SDK"
scioSdkExpected := "SCIO SDK"
template := fmt.Sprintf(
"Using "+
"{{if (eq .Sdk \"go\")}}%s{{end}}"+
"{{if (eq .Sdk \"python\")}}%s{{end}}"+
"{{if (eq .Sdk \"java\")}}%s{{end}}"+
"{{if (eq .Sdk \"scio\")}}%s{{end}}",
goSdkExpected, pythonSdkExpected, javaSdkExpected, scioSdkExpected)

goOrJavaExpected := "Text for Go or Java SDK"
templateAboutGoOrJava := fmt.Sprintf("{{if (eq .Sdk \"go\" \"java\")}}%s{{end}}", goOrJavaExpected)

for _, s := range []struct {
sdk tob.Sdk
template string
expected string
}{
{
sdk: tob.SDK_GO,
template: template,
expected: fmt.Sprintf("Using %s", goSdkExpected),
},
{
sdk: tob.SDK_PYTHON,
template: template,
expected: fmt.Sprintf("Using %s", pythonSdkExpected),
},
{
sdk: tob.SDK_JAVA,
template: template,
expected: fmt.Sprintf("Using %s", javaSdkExpected),
},
{
sdk: tob.SDK_SCIO,
template: template,
expected: fmt.Sprintf("Using %s", scioSdkExpected),
},
{
sdk: tob.SDK_GO,
template: templateAboutGoOrJava,
expected: goOrJavaExpected,
},
{
sdk: tob.SDK_JAVA,
template: templateAboutGoOrJava,
expected: goOrJavaExpected,
},
{
sdk: tob.SDK_SCIO,
template: templateAboutGoOrJava,
expected: "",
},
} {
res, err := processTemplate([]byte(s.template), s.sdk)
if err != nil {
panic(err)
}
assert.Equal(t, s.expected, string(res))
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
sdk: Java
sdk:
- Java
- Python
content:
- module 1
- module 2
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
sdk:
- Python
id: group1
name: The Group
complexity: BASIC
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
sdk:
- Python
id: challenge1
name: Challenge Name
complexity: BASIC
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
sdk:
- Python
id: example1
name: Example Unit Name
taskName: ExampleName
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
sdk:
- Python
id: intro-unit
name: Example Unit Name
taskName: ExampleName
Loading