blob: 970442711abf449d71684c83fef647e64f685126 [file]
// 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 fs_content
import (
"bytes"
"fmt"
"io/fs"
"log"
"os"
"path/filepath"
"regexp"
"text/template"
tob "beam.apache.org/learning/tour-of-beam/backend/internal"
)
const (
contentInfoYaml = "content-info.yaml"
moduleInfoYaml = "module-info.yaml"
groupInfoYaml = "group-info.yaml"
unitInfoYaml = "unit-info.yaml"
descriptionMd = "description.md"
hintMdRegexp = "hint[0-9]*.md"
)
type learningPathInfo struct {
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 {
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)
rootpath := filepath.Join(infopath, "..")
err = filepath.WalkDir(rootpath,
func(path string, d fs.DirEntry, err error) error {
switch {
// skip nested dirs
case path > rootpath && d.IsDir():
return filepath.SkipDir
case d.Name() == descriptionMd:
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()):
templateSource, err := os.ReadFile(path)
if err != nil {
return err
}
content, err := processTemplate(templateSource, ctx.sdk)
if err != nil {
return err
}
builder.AddHint(string(content))
}
return nil
})
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
}
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) (*tob.Node, error) {
files, err := os.ReadDir(rootpath)
if err != nil {
return nil, err
}
node := &tob.Node{}
for _, f := range files {
switch f.Name() {
case unitInfoYaml:
node.Type = tob.NODE_UNIT
node.Unit, err = collectUnit(filepath.Join(rootpath, unitInfoYaml), ctx)
case groupInfoYaml:
node.Type = tob.NODE_GROUP
node.Group, err = collectGroup(filepath.Join(rootpath, groupInfoYaml), ctx)
}
if err != nil {
return nil, err
}
}
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) {
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 nil, err
}
if node == nil {
continue
}
module.Nodes = append(module.Nodes, *node)
}
return &module, nil
}
func collectSdk(infopath string) (trees []tob.ContentTree, err error) {
info := loadLearningPathInfo(infopath)
sdks, err := getSupportedSdk(info.Sdk, infopath)
if err != nil {
return trees, 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)
}
}
trees = append(trees, tree)
}
return trees, nil
}
// Build a content tree for each SDK
// Walk recursively through the learning-content dir, search for metadata files:
// content-info.yaml, module-info.yaml, unit-info.yaml.
func CollectLearningTree(rootpath string) (trees []tob.ContentTree, err error) {
err = filepath.WalkDir(rootpath, func(path string, d fs.DirEntry, err error) error {
// terminate walk on any error
if err != nil {
return err
}
if d.Name() == contentInfoYaml {
collected, err := collectSdk(path)
if err != nil {
return err
}
trees = append(trees, collected...)
// don't walk into SDK subtree (already done by collectSdk)
return filepath.SkipDir
}
return nil
})
return trees, err
}
func isSupportedSdk(sdks []string, ctx *sdkContext, infopath string) (ok bool, err error) {
supportedSdks, err := getSupportedSdk(sdks, infopath)
if err != nil {
return false, err
}
for _, supportedSdk := range supportedSdks {
if ctx.sdk == supportedSdk {
return true, nil
}
}
return false, nil
}
func getSupportedSdk(sdk []string, infopath string) ([]tob.Sdk, error) {
sdks := make([]tob.Sdk, 0)
for _, s := range sdk {
curSdk := tob.ParseSdk(s)
if curSdk == tob.SDK_UNDEFINED {
return sdks, fmt.Errorf("unknown SDK at %v", infopath)
}
sdks = append(sdks, curSdk)
}
return sdks, nil
}