codegen/module.go (1,492 lines of code) (raw):
// Copyright (c) 2023 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package codegen
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"sort"
"strings"
"sync"
"sync/atomic"
yaml "github.com/ghodss/yaml"
"github.com/pkg/errors"
"github.com/uber/zanzibar/parallelize"
)
// moduleType enum defines whether a ModuleClass is a singleton or contains
// multiple directories with multiple configurations
type moduleClassType int
const (
// SingleModule defines a module class type that has 1 directory
SingleModule moduleClassType = iota
// MultiModule defines a module class type with multiple nested directories
MultiModule moduleClassType = iota
)
const jsonConfigSuffix = "-config.json"
const yamlConfigSuffix = "-config.yaml"
// NewModuleSystem returns a new module system
func NewModuleSystem(
moduleSearchPaths map[string][]string,
defaultDependencies map[string][]string,
selectiveBuilding bool,
postGenHook ...PostGenHook,
) *ModuleSystem {
return &ModuleSystem{
classes: map[string]*ModuleClass{},
classOrder: []string{},
postGenHook: postGenHook,
moduleSearchPaths: moduleSearchPaths,
defaultDependencies: defaultDependencies,
selectiveBuilding: selectiveBuilding,
}
}
// ModuleSystem defines the module classes and their type generators
type ModuleSystem struct {
classes map[string]*ModuleClass
classOrder []string
postGenHook []PostGenHook
moduleSearchPaths map[string][]string
defaultDependencies map[string][]string
selectiveBuilding bool
}
// Options Provides the features that are enabled while generating the build.
type Options struct {
EnableCustomInitialisation bool
CommitChange bool
QPSLevelsEnabled bool
CustomTemplates *Template
}
// PostGenHook provides a way to do work after the build is generated,
// useful to augment the build, e.g. generate mocks after interfaces are generated
type PostGenHook func(map[string][]*ModuleInstance) error
// RegisterClass defines a class of module in the module system
// For example, an "Endpoint" class or a "Client" class
func (system *ModuleSystem) RegisterClass(class ModuleClass) error {
name := class.Name
if name == "" {
return errors.Errorf("A module class name must not be empty")
}
if class.NamePlural == "" {
return errors.Errorf("A module class name plural must not be empty")
}
if system.classes[name] != nil {
return errors.Errorf("Module class %q is already defined", name)
}
class.types = map[string]BuildGenerator{}
system.classes[name] = &class
return nil
}
// dedup returns a slice containing unique sorted elements of the given slice
func dedup(array []string) []string {
dict := map[string]bool{}
for _, elt := range array {
dict[elt] = true
}
i := 0
unique := make([]string, len(dict))
for key := range dict {
unique[i] = key
i++
}
sort.Strings(unique)
return unique
}
// validateDir checks if the module class can map to the given dir
func (system *ModuleSystem) validateClassDir(class *ModuleClass, dir string) error {
dir = filepath.Clean(dir)
if strings.HasPrefix(dir, "..") {
return errors.Errorf(
"Module class %q must map to internal directories but found %q",
class.Name,
dir,
)
}
return nil
}
// RegisterClassType registers a type generator for a specific module class
// For example, the "http"" type generator for the "Endpoint"" class
func (system *ModuleSystem) RegisterClassType(
className string,
classType string,
generator BuildGenerator,
) error {
moduleClass := system.classes[className]
if moduleClass == nil {
return errors.Errorf(
"Cannot set class type %q for undefined class %q",
classType,
className,
)
}
if moduleClass.types[classType] != nil {
return errors.Errorf(
"The class type %q is already defined for class %q",
classType,
className,
)
}
moduleClass.types[classType] = generator
return nil
}
func (system *ModuleSystem) populateResolvedDependencies(
classInstances map[string]*ModuleInstance,
resolvedModules map[string]map[string]*ModuleInstance,
) error {
// Resolve the class dependencies
for _, classInstance := range classInstances {
for _, classDependency := range classInstance.Dependencies {
moduleClassInstances, ok :=
resolvedModules[classDependency.ClassName]
if !ok {
return errors.Errorf(
"Invalid class name %q in dependencies for %q %q",
classDependency.ClassName,
classInstance.ClassName,
classInstance.InstanceName,
)
}
dependencyInstance := moduleClassInstances[classDependency.InstanceName]
if dependencyInstance == nil {
return errors.Errorf(
"Unknown %q class dependency %q "+
"in dependencies for %q %q",
classDependency.ClassName,
classDependency.InstanceName,
classInstance.ClassName,
classInstance.InstanceName,
)
}
resolvedDependencies, ok :=
classInstance.ResolvedDependencies[classDependency.ClassName]
if !ok {
resolvedDependencies = []*ModuleInstance{}
}
classInstance.ResolvedDependencies[classDependency.ClassName] =
appendUniqueModule(resolvedDependencies, dependencyInstance)
}
// Sort the dependencies for deterministic code generation
for className, deps := range classInstance.ResolvedDependencies {
sortedModuleList, err := sortDependencyList(
className,
deps,
)
if err != nil {
return err
}
classInstance.ResolvedDependencies[className] = sortedModuleList
}
}
return nil
}
func appendUniqueModule(
classDeps []*ModuleInstance,
instance *ModuleInstance,
) []*ModuleInstance {
for i, classInstance := range classDeps {
if classInstance.InstanceName == instance.InstanceName {
classDeps[i] = instance
return classDeps
}
}
return append(classDeps, instance)
}
func (system *ModuleSystem) populateRecursiveDependencies(
instances map[string]*ModuleInstance,
) error {
for _, classInstance := range instances {
recursiveDeps := map[string]map[string]*ModuleInstance{}
err := resolveRecursiveDependencies(
classInstance,
recursiveDeps,
)
if err != nil {
return err
}
for _, className := range system.classOrder {
moduleMap, ok := recursiveDeps[className]
if !ok {
continue
}
// If a glob pattern matches the same module class multiple times, make sure that DependencyOrder
// remains the unique list of classes.
found := false
for _, dependencyOrderClassName := range classInstance.DependencyOrder {
if dependencyOrderClassName == className {
found = true
break
}
}
if !found {
classInstance.DependencyOrder = append(
classInstance.DependencyOrder,
className,
)
}
moduleList := make([]*ModuleInstance, len(moduleMap))
index := 0
for _, moduleInstance := range moduleMap {
moduleList[index] = moduleInstance
index++
}
sortedModuleList, err := sortDependencyList(className, moduleList)
if err != nil {
return err
}
classInstance.RecursiveDependencies[className] = sortedModuleList
}
}
return nil
}
func resolveRecursiveDependencies(
instance *ModuleInstance,
resolvedDeps map[string]map[string]*ModuleInstance,
) error {
for className, depList := range instance.ResolvedDependencies {
classDeps := resolvedDeps[className]
if classDeps == nil {
classDeps = map[string]*ModuleInstance{}
resolvedDeps[className] = classDeps
}
for _, dep := range depList {
if classDeps[dep.InstanceName] == nil {
classDeps[dep.InstanceName] = dep
err := resolveRecursiveDependencies(dep, resolvedDeps)
if err != nil {
return err
}
}
}
}
return nil
}
type sortableDependencyList []*ModuleInstance
func (s sortableDependencyList) Len() int {
return len(s)
}
func (s sortableDependencyList) Less(i int, j int) bool {
return s[i].InstanceName < s[j].InstanceName
}
func (s sortableDependencyList) Swap(i int, j int) {
s[i], s[j] = s[j], s[i]
}
func sortDependencyList(
className string,
instances []*ModuleInstance,
) ([]*ModuleInstance, error) {
instanceList := sortableDependencyList(instances[:])
sort.Sort(instanceList)
sorted := make([]*ModuleInstance, len(instances))
for i, instance := range instances {
insertIndex := i
for j := 0; j < i; j++ {
if insertIndex == i {
if peerDepends(sorted[j], instance) {
insertIndex = j
}
}
if insertIndex != i {
if peerDepends(instance, sorted[j]) {
return nil, errors.Errorf(
"Dependency cycle: %s cannot be initialized before %s",
sorted[j].InstanceName,
instance.InstanceName,
)
}
}
}
for shuffle := i; shuffle > insertIndex; shuffle-- {
sorted[shuffle] = sorted[shuffle-1]
}
sorted[insertIndex] = instance
}
return sorted, nil
}
// peerDepends returns true if module a and module b have the same class name
// and a requires b
func peerDepends(a *ModuleInstance, b *ModuleInstance) bool {
if a.ClassName != b.ClassName {
return false
}
for _, dependency := range a.RecursiveDependencies[a.ClassName] {
if dependency.InstanceName == b.InstanceName &&
dependency.ClassName == b.ClassName {
return true
}
}
return false
}
type byName []*ModuleClass
func (n byName) Len() int { return len(n) }
func (n byName) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
func (n byName) Less(i, j int) bool { return n[i].Name < n[j].Name }
// sort the module classes by class height in DAG, lowest node first
func sortModuleClasses(classes []*ModuleClass) ([]*ModuleClass, error) {
heightMap := map[*ModuleClass]int{}
sortedGroup := [][]*ModuleClass{}
sort.Sort(byName(classes))
for _, class := range classes {
h, err := height(class, heightMap, []*ModuleClass{})
if err != nil {
return nil, err
}
l := len(sortedGroup)
if l < h+1 {
for i := 0; i < h+1-l; i++ {
sortedGroup = append(sortedGroup, []*ModuleClass{})
}
}
sortedGroup[h] = append(sortedGroup[h], class)
}
sorted := []*ModuleClass{}
for _, g := range sortedGroup {
sort.Sort(byName(g))
sorted = append(sorted, g...)
}
return sorted, nil
}
// height marks the height of each module class height in the dependency tree
func height(i *ModuleClass, known map[*ModuleClass]int, seen []*ModuleClass) (int, error) {
// detect dependency cycle
for _, class := range seen {
if i == class {
var path string
for _, c := range seen {
path += c.Name + "->"
}
path += i.Name
return 0, fmt.Errorf("dependency cycle detected for module class %q: %s", i.Name, path)
}
}
if h, ok := known[i]; ok {
return h, nil
}
if len(i.dependentClasses) == 0 {
known[i] = 0
return 0, nil
}
mh := 0
seen = append(seen, i)
for _, instance := range i.dependentClasses {
ch, err := height(instance, known, seen)
if err != nil {
return 0, err
}
if ch > mh {
mh = ch
}
}
known[i] = mh + 1
return mh + 1, nil
}
func appendUniqueClass(list []*ModuleClass, toAppend *ModuleClass) []*ModuleClass {
for _, value := range list {
if toAppend == value {
return list
}
}
return append(list, toAppend)
}
// populateClassDependencies consolidates the dependencies claimed by module classes
func (system *ModuleSystem) populateClassDependencies() error {
for _, c := range system.classes {
for _, d := range c.DependsOn {
dc, ok := system.classes[d]
if !ok {
return fmt.Errorf("module class %q depends on %q which is not defined", c.Name, d)
}
c.dependentClasses = appendUniqueClass(c.dependentClasses, dc)
}
for _, d := range c.DependedBy {
dc, ok := system.classes[d]
if !ok {
return fmt.Errorf("module class %q is depended by %q which is not defined", c.Name, d)
}
dc.dependentClasses = appendUniqueClass(dc.dependentClasses, c)
}
}
return nil
}
// resolveClassOrder sorts the registered classes by dependency and sets
// classOrder field
func (system *ModuleSystem) resolveClassOrder() error {
err := system.populateClassDependencies()
if err != nil {
return errors.Wrap(err, "error resolving module class order")
}
classes := make([]*ModuleClass, len(system.classes))
i := 0
for _, c := range system.classes {
classes[i] = c
i++
}
sorted, err := sortModuleClasses(classes)
if err != nil {
return errors.Wrap(err, "error resolving module class order")
}
system.classOrder = make([]string, len(system.classes))
for i, c := range sorted {
system.classOrder[i] = c.Name
}
return nil
}
// Given a module instance name like "app/foo/clients/bar", strip the "clients" part, i.e. usually the
// second to last path part.
func stripModuleClassName(classType, moduleDir string) string {
parts := strings.Split(moduleDir, string(filepath.Separator))
if len(parts) == 1 {
return moduleDir
}
for i := len(parts) - 1; i >= 0; i-- {
if parts[i] == classType {
// Shift parts left starting with ith element, overwriting parts[i]
for j := i + 1; j < len(parts); j++ {
parts[j-1] = parts[j]
}
parts = parts[0 : len(parts)-1]
break
}
}
return strings.Join(parts, string(filepath.Separator))
}
// ResolveModules resolves the module instances from the config on disk
// Using the system class and type definitions, the class directories are
// walked, and a module instance is initialized for each identified module in
// the target directory.
func (system *ModuleSystem) ResolveModules(
packageRoot string,
baseDirectory string,
targetGenDir string,
options Options,
) (map[string][]*ModuleInstance, error) {
// resolve module class order before read and resolve each module
if err := system.resolveClassOrder(); err != nil {
return nil, err
}
resolvedModules := map[string]map[string]*ModuleInstance{}
// system.classOrder is important. Downstream dependencies **must** be resolved first
for _, className := range system.classOrder {
defaultDependencies, err := system.getDefaultDependencies(
baseDirectory,
className,
system.defaultDependencies,
system.moduleSearchPaths,
)
if err != nil {
return nil, errors.Wrapf(err, "error getting default dependencies for class %s", className)
}
for _, moduleDirectoryGlob := range system.moduleSearchPaths[className] {
moduleDirectoriesAbs, err := filepath.Glob(filepath.Join(baseDirectory, moduleDirectoryGlob))
if err != nil {
return nil, errors.Wrapf(err, "error globbing %q", moduleDirectoryGlob)
}
runner := parallelize.NewUnboundedRunner(len(moduleDirectoriesAbs))
for _, moduleDirAbs := range moduleDirectoriesAbs {
f := system.getInstanceFunc(baseDirectory, className, packageRoot, targetGenDir, defaultDependencies, moduleDirectoryGlob, options)
wrk := ¶llelize.SingleParamWork{Data: moduleDirAbs, Func: f}
runner.SubmitWork(wrk)
}
results, err := runner.GetResult()
if err != nil {
return nil, err
}
for _, instanceInf := range results {
if instanceInf == nil {
continue
}
instance := instanceInf.(*ModuleInstance)
instanceMap := resolvedModules[instance.ClassName]
if instanceMap == nil {
instanceMap = map[string]*ModuleInstance{}
resolvedModules[instance.ClassName] = instanceMap
}
instanceMap[instance.InstanceName] = instance
}
// Resolve dependencies for all classes
resolveErr := system.populateResolvedDependencies(
resolvedModules[className],
resolvedModules,
)
if resolveErr != nil {
return nil, resolveErr
}
// Resolved recursive dependencies for all classes
recursiveErr := system.populateRecursiveDependencies(resolvedModules[className])
if recursiveErr != nil {
return nil, recursiveErr
}
}
}
classArrayModuleMap := map[string][]*ModuleInstance{}
for className, instanceMap := range resolvedModules {
for _, instance := range instanceMap {
classArrayModuleMap[className] = append(classArrayModuleMap[className], instance)
}
}
return classArrayModuleMap, nil
}
func (system *ModuleSystem) getInstanceFunc(baseDirectory string, className string, packageRoot string, targetGenDir string, defaultDependencies []ModuleDependency, moduleDirectoryGlob string, options Options) func(moduleDirAbsInf interface{}) (interface{}, error) {
f := func(moduleDirAbsInf interface{}) (interface{}, error) {
moduleDirAbs := moduleDirAbsInf.(string)
stat, err := os.Stat(moduleDirAbs)
if err != nil {
return nil, errors.Wrapf(
err,
"internal error: cannot stat %q",
moduleDirAbs,
)
}
if !stat.IsDir() {
// If a *-config.yaml file, or any other metadata file also matched the glob, skip it, since we are
// interested only in the containing directories.
return nil, nil
}
moduleDir, err := filepath.Rel(baseDirectory, moduleDirAbs)
if err != nil {
return nil, errors.Wrapf(
err,
"internal error: cannot make %q relative to %q",
moduleDirAbs,
baseDirectory,
)
}
classConfigPath, _, _ := getConfigFilePath(moduleDirAbs, className)
if classConfigPath == "" {
fmt.Printf(" no class config found in %s directory\n", moduleDirAbs)
// No class config found in this directory, skip over it
return nil, nil
}
instance, instanceErr := system.readInstance(
className,
packageRoot,
baseDirectory,
targetGenDir,
moduleDir,
moduleDirAbs,
defaultDependencies,
options,
)
if instanceErr != nil {
return nil, errors.Wrapf(
instanceErr,
"Error reading multi instance %q",
moduleDir,
)
}
if className != instance.ClassName {
return nil, fmt.Errorf(
"invariant: all instances in a multi-module directory %q are of type %q (violated by %q)",
moduleDirectoryGlob,
className,
moduleDir,
)
}
return instance, nil
}
return f
}
func (system *ModuleSystem) getDefaultDependencies(
baseDirectory string,
className string,
dependencyGlobs map[string][]string,
moduleSearchPaths map[string][]string,
) ([]ModuleDependency, error) {
defaultDependencies := make([]ModuleDependency, 0)
for _, defaultDepDirGlob := range dependencyGlobs[className] {
defaultDepDirsAbs, err := filepath.Glob(filepath.Join(baseDirectory, defaultDepDirGlob))
if err != nil {
return nil, errors.Wrapf(err, "error globbing default dependency %q", defaultDepDirGlob)
}
ch := make(chan defaultDependencyRes, len(defaultDepDirsAbs))
var wg sync.WaitGroup
wg.Add(len(defaultDepDirsAbs))
for _, defaultDepDirAbs := range defaultDepDirsAbs {
go system.getDefaultDependency(baseDirectory, defaultDepDirAbs, moduleSearchPaths,
className, ch, &wg)
}
go func() {
wg.Wait()
close(ch)
}()
for ele := range ch {
if ele.err != nil {
return nil, ele.err
}
if ele.dep == nil {
continue
}
defaultDependencies = append(defaultDependencies, *ele.dep)
}
}
return defaultDependencies, nil
}
type defaultDependencyRes struct {
err error
dep *ModuleDependency
}
func (system *ModuleSystem) getDefaultDependency(baseDirectory string, defaultDepDirAbs string,
moduleSearchPaths map[string][]string, className string, ch chan defaultDependencyRes, wg *sync.WaitGroup) {
defer wg.Done()
dependencyClassName, err := getClassNameOfDependency(baseDirectory, defaultDepDirAbs, moduleSearchPaths)
if err != nil {
ch <- defaultDependencyRes{
err: errors.Wrapf(
err,
"cannot get class name of dependency %s",
defaultDepDirAbs,
),
}
return
}
found := false
for _, actualClassDependencyName := range system.classes[className].DependsOn {
if dependencyClassName == actualClassDependencyName {
found = true
break
}
}
if !found {
ch <- defaultDependencyRes{err: errors.Errorf(
"default dependency class %s is not a dependency of %s",
dependencyClassName,
className,
),
}
return
}
classConfigPath, _, _ := getConfigFilePath(defaultDepDirAbs, dependencyClassName)
if classConfigPath == "" {
// No class config found, skip over this directory
ch <- defaultDependencyRes{}
return
}
dependencyDir, err := filepath.Rel(baseDirectory, defaultDepDirAbs)
if err != nil {
ch <- defaultDependencyRes{err: errors.Wrapf(
err,
"cannot make %q relative to %q for default dependency",
defaultDepDirAbs,
baseDirectory,
),
}
return
}
defaultDependency, err := system.getModuleDependency(classConfigPath, dependencyDir, dependencyClassName)
if err != nil {
ch <- defaultDependencyRes{err: errors.Wrapf(
err,
"error getting default dependency module %s for class %s",
defaultDepDirAbs,
className,
),
}
return
}
ch <- defaultDependencyRes{dep: defaultDependency}
return
}
func (system *ModuleSystem) getModuleDependency(
classConfigPath string,
dependencyDir string,
className string,
) (*ModuleDependency, error) {
raw, readErr := ioutil.ReadFile(classConfigPath)
if readErr != nil {
return nil, errors.Wrapf(
readErr,
"error reading ClassConfig %q",
classConfigPath,
)
}
_, configErr := NewClassConfig(raw)
if configErr != nil {
return nil, errors.Wrapf(
configErr,
"error unmarshal ClassConfig %q",
classConfigPath,
)
}
var classPlural string
for _, moduleClass := range system.classes {
if moduleClass.Name == className {
classPlural = moduleClass.NamePlural
}
}
moduleDependency := ModuleDependency{
ClassName: className,
InstanceName: stripModuleClassName(classPlural, dependencyDir),
}
return &moduleDependency, nil
}
func getClassNameOfDependency(
baseDirectory string,
dependencyDirectory string,
moduleSearchPaths map[string][]string,
) (string, error) {
for className, moduleGlobs := range moduleSearchPaths {
for _, moduleGlob := range moduleGlobs {
matched, err := filepath.Match(filepath.Join(baseDirectory, moduleGlob), dependencyDirectory)
if err != nil {
return "", errors.Wrapf(
err,
"error matching module glob %s with dependency directory %s",
moduleGlob,
dependencyDirectory,
)
}
if matched {
return className, nil
}
}
}
return "", errors.Errorf("could not find class for default dependency %s", dependencyDirectory)
}
func getConfigFilePath(dir, name string) (string, string, string) {
yamlFileName := name + yamlConfigSuffix
jsonFileName := ""
path := filepath.Join(dir, yamlFileName)
if _, err := os.Stat(path); os.IsNotExist(err) {
// Cannot find yaml file, try json file instead
jsonFileName = name + jsonConfigSuffix
yamlFileName = ""
path = filepath.Join(dir, jsonFileName)
if _, err := os.Stat(path); os.IsNotExist(err) {
// Cannot find any config file
path = ""
jsonFileName = ""
yamlFileName = ""
}
}
return path, yamlFileName, jsonFileName
}
func (system *ModuleSystem) readInstance(
className string,
packageRoot string,
baseDirectory string,
targetGenDir string,
instanceDirectory string,
classConfigDir string,
defaultDependencies []ModuleDependency,
options Options,
) (*ModuleInstance, error) {
classConfigPath, yamlFileName, jsonFileName := getConfigFilePath(classConfigDir, className)
raw, readErr := ioutil.ReadFile(classConfigPath)
if readErr != nil {
return nil, errors.Wrapf(
readErr,
"Error reading ClassConfig %q",
classConfigPath,
)
}
jsonRaw := []byte{}
if jsonFileName != "" {
jsonRaw = raw
}
config, configErr := NewClassConfig(raw)
if configErr != nil {
return nil, errors.Wrapf(
configErr,
"Error unmarshal ClassConfig %q",
classConfigPath,
)
}
dependencies := readDeps(config.Dependencies)
dependencies = append(dependencies, defaultDependencies...)
var classPlural string
for _, moduleClass := range system.classes {
if moduleClass.Name == className {
classPlural = moduleClass.NamePlural
}
}
instanceName := stripModuleClassName(classPlural, instanceDirectory)
packageInfo, err := readPackageInfo(
packageRoot,
baseDirectory,
targetGenDir,
className,
instanceDirectory,
config,
options,
)
if err != nil {
// TODO: We should accumulate errors and list them all here
// Expected $class-config.yaml to exist in ...
return nil, errors.Wrapf(
err,
"Error reading class package info for %q %q",
className,
instanceName,
)
}
return &ModuleInstance{
PackageInfo: packageInfo,
ClassName: className,
ClassType: config.Type,
BaseDirectory: baseDirectory,
Directory: instanceDirectory,
InstanceName: instanceName,
Dependencies: dependencies,
ResolvedDependencies: map[string][]*ModuleInstance{},
RecursiveDependencies: map[string][]*ModuleInstance{},
DependencyOrder: []string{},
JSONFileName: jsonFileName,
YAMLFileName: yamlFileName,
JSONFileRaw: jsonRaw,
YAMLFileRaw: raw,
Config: config.Config,
SelectiveBuilding: config.SelectiveBuilding,
CustomTemplates: options.CustomTemplates,
}, nil
}
func readDeps(yamlDeps map[string][]string) []ModuleDependency {
depCount := 0
for _, depsList := range yamlDeps {
depCount += len(depsList)
}
deps := make([]ModuleDependency, depCount)
depIndex := 0
for className, depsList := range yamlDeps {
for _, instanceName := range depsList {
deps[depIndex] = ModuleDependency{
ClassName: className,
InstanceName: instanceName,
}
depIndex++
}
}
return deps
}
func readPackageInfo(
packageRoot string,
baseDirectory string,
targetGenDir string,
className string,
instanceDirectory string,
config *ClassConfig,
options Options,
) (*PackageInfo, error) {
qualifiedClassName := strings.Title(CamelCase(className))
qualifiedInstanceName := strings.Title(CamelCase(config.Name))
defaultAlias := packageName(qualifiedInstanceName + qualifiedClassName)
relativeGeneratedPath, err := filepath.Rel(baseDirectory, targetGenDir)
if err != nil {
return nil, errors.Wrapf(
err,
"Error computing generated import string for %q",
targetGenDir,
)
}
// The module system presently has special reservations for the "custom"
// type. We should really extrapolate from the class type info what the
// default export type is for this instance
var isExportGenerated bool
if config.Type == "custom" {
if config.IsExportGenerated == nil {
isExportGenerated = false
} else {
isExportGenerated = *config.IsExportGenerated
}
} else if config.IsExportGenerated == nil {
isExportGenerated = true
} else {
isExportGenerated = *config.IsExportGenerated
}
var customInitialisation bool
if config.CustomInitialisation == nil || options.EnableCustomInitialisation == false {
customInitialisation = false
} else {
customInitialisation = *config.CustomInitialisation
}
return &PackageInfo{
// The package name is assumed to be the lower case of the instance
// Name plus the titular class name, such as fooClient
PackageName: defaultAlias,
// The prefixes "Static" and "Generated" are used to ensure global
// uniqueness of the provided package aliases. Note that the default
// package is "PackageName".
PackageAlias: defaultAlias + "static",
PackageRoot: packageRoot,
GeneratedPackageAlias: defaultAlias + "generated",
GeneratedModulePackageAlias: defaultAlias + "module",
PackagePath: path.Join(
packageRoot,
instanceDirectory,
),
GeneratedPackagePath: filepath.Join(
packageRoot,
relativeGeneratedPath,
instanceDirectory,
),
GeneratedModulePackagePath: filepath.Join(
packageRoot,
relativeGeneratedPath,
instanceDirectory,
"module",
),
ExportName: "New" + qualifiedClassName,
InitializerName: "Initialize" + qualifiedClassName,
QualifiedInstanceName: qualifiedInstanceName,
ExportType: qualifiedClassName,
IsExportGenerated: isExportGenerated,
CustomInitialisation: customInitialisation,
}, nil
}
func (system *ModuleSystem) populateSpec(instance *ModuleInstance) error {
classGenerators := system.classes[instance.ClassName]
generator := classGenerators.types[instance.ClassType]
if generator == nil {
return nil
}
specProvider, ok := generator.(SpecProvider)
if !ok {
fmt.Fprintf(
os.Stderr,
"warning: %q %q generator is not a spec provider, cannot use incremental builds\n",
instance.ClassName,
instance.ClassType,
)
return fmt.Errorf("%q %q generator does not implement SpecProvider interface", instance.ClassName,
instance.ClassType)
}
spec, err := specProvider.ComputeSpec(instance)
if err != nil {
return err
}
if spec != nil {
instance.mu.Lock()
instance.genSpec = spec
instance.mu.Unlock()
}
// HACK: to get get of bad modules, which should not be there at first place
filterNilClientDeps(instance)
return nil
}
func filterNilClientDeps(in *ModuleInstance) {
filterNilClientDepsHelper(in.ResolvedDependencies)
filterNilClientDepsHelper(in.RecursiveDependencies)
}
func filterNilClientDepsHelper(deps map[string][]*ModuleInstance) {
moduleInstances, ok := deps["client"]
if !ok {
return
}
var validClients []*ModuleInstance
for _, modInstance := range moduleInstances {
if modInstance.GeneratedSpec() == nil {
continue
}
validClients = append(validClients, modInstance)
}
deps["client"] = validClients
}
// collectTransitiveDependencies will collect every instance in resolvedModules that depends on something in initialInstances.
func (system *ModuleSystem) collectTransitiveDependencies(
initialInstances []ModuleDependency,
allModules map[string][]*ModuleInstance,
) (map[string][]*ModuleInstance, error) {
toBeBuiltModules := make(map[ModuleDependency]*ModuleInstance, 0)
for _, className := range system.classOrder {
// Convert every ModuleDependency to its corresponding *ModuleInstance
for _, initialInstance := range initialInstances {
for _, instance := range allModules[className] {
if initialInstance.equal(instance) {
toBeBuiltModules[instance.AsModuleDependency()] = instance
break
}
}
}
// Collect all the ModuleInstances that depend on anything from toBeBuiltModules
for _, dependentInstance := range toBeBuiltModules {
for _, instance := range allModules[className] {
classInstanceTransitives := instance.RecursiveDependencies[dependentInstance.ClassName]
for _, classInstanceDependency := range classInstanceTransitives {
if !classInstanceDependency.equal(dependentInstance) {
continue
}
toBeBuiltModules[instance.AsModuleDependency()] = instance
break
}
}
}
}
toBeBuiltModulesList := make(map[string][]*ModuleInstance)
for _, instance := range toBeBuiltModules {
if _, ok := toBeBuiltModulesList[instance.ClassName]; !ok {
toBeBuiltModulesList[instance.ClassName] = make([]*ModuleInstance, 0)
}
toBeBuiltModulesList[instance.ClassName] = append(toBeBuiltModulesList[instance.ClassName], instance)
}
system.trimToSelectiveDependencies(toBeBuiltModulesList)
return toBeBuiltModulesList, nil
}
func instanceFQN(instance *ModuleInstance) string {
return fmt.Sprintf("%s.%s", instance.ClassName, instance.InstanceName)
}
// IncrementalBuild is like Build but filtered to only the given module instances.
func (system *ModuleSystem) IncrementalBuild(
packageRoot,
baseDirectory,
targetGenDir string,
instances []ModuleDependency,
resolvedModules map[string][]*ModuleInstance,
options Options,
) (map[string][]*ModuleInstance, error) {
skipModuleMap := map[*ModuleInstance]struct{}{}
toBeBuiltModules := make(map[string][]*ModuleInstance)
for _, className := range system.classOrder {
var wg sync.WaitGroup
wg.Add(len(resolvedModules[className]))
ch := make(chan *populateSpecRes, len(resolvedModules[className]))
for _, instance := range resolvedModules[className] {
go func(instance *ModuleInstance) {
defer wg.Done()
if err := system.populateSpec(instance); err != nil {
baseErr := errors.Cause(err)
if _, ok := baseErr.(*IgnorePopulateSpecStageErr); ok {
return
}
if _, ok := baseErr.(*ErrorSkipCodeGen); ok {
// HACK: to get get of bad modules, which should not be even be loaded in dag at first place
ch <- &populateSpecRes{mi: instance}
} else {
ch <- &populateSpecRes{err: err}
}
}
}(instance)
}
go func() {
wg.Wait()
close(ch)
}()
for psRes := range ch {
if psRes.err != nil {
return nil, psRes.err
}
skipModuleMap[psRes.mi] = struct{}{}
}
}
resolvedModulesCopy := map[string][]*ModuleInstance{}
for cls, modules := range resolvedModules {
for _, m := range modules {
if _, ok := skipModuleMap[m]; ok {
// skipping error modules
fmt.Println("skipping module gen", m.InstanceName)
continue
}
m.RecursiveDependencies = trimSkipDependencies(m.RecursiveDependencies, skipModuleMap)
m.ResolvedDependencies = trimSkipDependencies(m.ResolvedDependencies, skipModuleMap)
resolvedModulesCopy[cls] = append(resolvedModulesCopy[cls], m)
}
}
resolvedModules = resolvedModulesCopy
if instances == nil || len(instances) == 0 {
for _, modules := range resolvedModules {
for _, instance := range modules {
instances = append(instances, instance.AsModuleDependency())
}
}
}
// If toBeBuiltModules is not empty already, it is likely that one of the modules does not implement
// the SpecProvider interface, hence incremental build is not possible.
if len(toBeBuiltModules) == 0 {
var err error
toBeBuiltModules, err = system.collectTransitiveDependencies(instances,
resolvedModules)
if err != nil {
// if incrementalBuild fails, perform a full build.
fmt.Printf("Falling back to full build due to err: %s\n", err.Error())
toBeBuiltModules = resolvedModules
}
}
moduleCount := 0
var moduleIndex int32
for _, moduleList := range toBeBuiltModules {
moduleCount += len(moduleList)
}
qpsLevels, err := PopulateQPSLevels(baseDirectory+"/endpoints", options.QPSLevelsEnabled)
if err != nil {
return nil, errors.Errorf(
"error in populating qps levels for base directory %q",
baseDirectory,
)
}
for _, class := range system.classOrder {
var wg sync.WaitGroup
wg.Add(len(toBeBuiltModules[class]))
ch := make(chan error, len(toBeBuiltModules[class]))
for _, moduleInstance := range toBeBuiltModules[class] {
go func(moduleInstance *ModuleInstance) {
defer wg.Done()
physicalGenDir := filepath.Join(targetGenDir, moduleInstance.Directory)
prettyDir, _ := filepath.Rel(baseDirectory, physicalGenDir)
PrintGenLine(moduleInstance.ClassType, moduleInstance.ClassName, moduleInstance.InstanceName,
prettyDir, int(atomic.AddInt32(&moduleIndex, 1)), moduleCount)
moduleInstance.QPSLevels = qpsLevels
if err := system.Build(packageRoot, baseDirectory, physicalGenDir, moduleInstance, options); err != nil {
ch <- err
}
}(moduleInstance)
}
go func() {
wg.Wait()
close(ch)
}()
for err := range ch {
if err != nil {
return nil, err
}
}
}
var wg sync.WaitGroup
ch := make(chan error, len(system.postGenHook))
for i, hook := range system.postGenHook {
if hook != nil {
wg.Add(1)
go func(hook PostGenHook) {
defer wg.Done()
if err := hook(toBeBuiltModules); err != nil {
ch <- errors.Wrapf(err, "error running %dth post generation hook", i)
}
}(hook)
}
}
go func() {
wg.Wait()
close(ch)
}()
for err := range ch {
if err != nil {
return toBeBuiltModules, err
}
}
return toBeBuiltModules, nil
}
func trimSkipDependencies(depsMap map[string][]*ModuleInstance, skipModuleMap map[*ModuleInstance]struct{}) map[string][]*ModuleInstance {
newDepsMap := map[string][]*ModuleInstance{}
for cl, deps := range depsMap {
for _, d := range deps {
if _, ok := skipModuleMap[d]; ok {
continue
}
newDepsMap[cl] = append(newDepsMap[cl], d)
}
}
return newDepsMap
}
type populateSpecRes struct {
mi *ModuleInstance
err error
}
func (system *ModuleSystem) trimToSelectiveDependencies(toBeBuiltModules map[string][]*ModuleInstance) {
if system.selectiveBuilding {
selectiveModuleInstances := system.getSelectiveModules(toBeBuiltModules)
toBuiltMap := flattenInstances(toBeBuiltModules)
for _, instance := range selectiveModuleInstances {
instance.ResolvedDependencies = resolvedSelectiveDependencies(instance, toBuiltMap)
instance.RecursiveDependencies = recursiveSelectiveDependencies(instance)
}
}
}
func (system *ModuleSystem) getSelectiveModules(toBeBuiltModules map[string][]*ModuleInstance) map[string]*ModuleInstance {
selectiveModuleInstances := map[string]*ModuleInstance{}
for _, instances := range toBeBuiltModules {
for _, instance := range instances {
if instance.SelectiveBuilding {
selectiveModuleInstances[instanceFQN(instance)] = instance
}
}
}
return selectiveModuleInstances
}
// recursiveSelectiveDependencies gets a recursive dependencies of resolved (direct)
// dependencies including direct dependencies itself
func recursiveSelectiveDependencies(instance *ModuleInstance) map[string][]*ModuleInstance {
filteredRecursiveMap := map[string]*ModuleInstance{}
for _, resolvedDependencies := range instance.ResolvedDependencies {
for _, resolvedInstance := range resolvedDependencies {
for _, recursiveDependencies := range resolvedInstance.RecursiveDependencies {
for _, recursiveInstance := range recursiveDependencies {
filteredRecursiveMap[instanceFQN(recursiveInstance)] = recursiveInstance
}
}
filteredRecursiveMap[instanceFQN(resolvedInstance)] = resolvedInstance
}
}
filteredRecursiveModules := map[string][]*ModuleInstance{}
for _, instance := range filteredRecursiveMap {
filteredRecursiveModules[instance.ClassName] = append(filteredRecursiveModules[instance.ClassName], instance)
}
return filteredRecursiveModules
}
// resolvedSelectiveDependencies gets a subset of resolved dependencies (direct) of a instance which needs to be built
func resolvedSelectiveDependencies(instance *ModuleInstance, toBuiltMap map[string]*ModuleInstance) map[string][]*ModuleInstance {
filteredResolvedModules := map[string][]*ModuleInstance{}
for _, resolvedDependencies := range instance.ResolvedDependencies {
for _, resolvedInstance := range resolvedDependencies {
if _, ok := toBuiltMap[instanceFQN(resolvedInstance)]; ok {
filteredResolvedModules[resolvedInstance.ClassName] = append(
filteredResolvedModules[resolvedInstance.ClassName], resolvedInstance)
}
}
}
// if after filtering there are no dependencies, we will default it to all dependencies.
// this is done as when a module alone changes toBuilt would not have any dependencies
if len(filteredResolvedModules) == 0 {
filteredResolvedModules = instance.ResolvedDependencies
}
return filteredResolvedModules
}
func flattenInstances(resolvedModules map[string][]*ModuleInstance) map[string]*ModuleInstance {
flattenedMap := map[string]*ModuleInstance{}
for _, instances := range resolvedModules {
for _, instance := range instances {
flattenedMap[instanceFQN(instance)] = instance
}
}
return flattenedMap
}
// Build invokes the generator for a module instance and optionally writes the files to disk
func (system *ModuleSystem) Build(packageRoot string, baseDirectory string, physicalGenDir string,
instance *ModuleInstance, options Options) error {
classGenerators := system.classes[instance.ClassName]
generator := classGenerators.types[instance.ClassType]
if generator == nil || instance.PackageInfo.CustomInitialisation {
fmt.Fprintf(
os.Stderr,
"Skipping generation of %q %q class of type %q "+
"as generator is not defined\n",
instance.InstanceName,
instance.ClassName,
instance.ClassType,
)
return nil
}
buildResult, err := generator.Generate(instance)
if err != nil {
fmt.Fprintf(
os.Stderr,
"Error generating %q %q of type %q:\n%s\n",
instance.InstanceName,
instance.ClassName,
instance.ClassType,
err.Error(),
)
return err
}
if buildResult == nil {
return nil
}
instance.mu.Lock()
instance.genSpec = buildResult.Spec
instance.mu.Unlock()
if !options.CommitChange {
return nil
}
runner := parallelize.NewUnboundedRunner(len(buildResult.Files))
for filePath, content := range buildResult.Files {
f := func(filePathInf interface{}, contentInf interface{}) (interface{}, error) {
filePath := filePathInf.(string)
content := contentInf.([]byte)
filePath = filepath.Clean(filePath)
resolvedPath := filepath.Join(
physicalGenDir,
filePath,
)
if err := writeFile(resolvedPath, content); err != nil {
return nil, errors.Wrapf(
err,
"Error writing to file %q",
resolvedPath,
)
}
// HACK: The module system writer shouldn't
// assume that we want to format the files in
// this way, but we don't have these formatters
// as a library or a custom post build script
// for the generators yet.
if filepath.Ext(filePath) == ".go" {
if err := FormatGoFile(resolvedPath); err != nil {
return nil, err
}
}
return nil, nil
}
wrk := ¶llelize.TwoParamWork{Data1: filePath, Data2: content, Func: f}
runner.SubmitWork(wrk)
}
_, err = runner.GetResult()
if err != nil {
return err
}
return nil
}
// GenerateBuild will, given a module system configuration directory and a
// target build directory, run the generators assigned to each type of module
// and write the generated output to the module build directory if commitChange
//
// Deprecated: Use Build instead.
func (system *ModuleSystem) GenerateBuild(
packageRoot string,
baseDirectory string,
targetGenDir string,
options Options,
) (map[string][]*ModuleInstance, error) {
resolvedModules, err := system.ResolveModules(
packageRoot,
baseDirectory,
targetGenDir,
options,
)
if err != nil {
return nil, err
}
moduleCount := 0
moduleIndex := 0
for _, moduleList := range resolvedModules {
moduleCount += len(moduleList)
}
for _, class := range system.classOrder {
for _, moduleInstance := range resolvedModules[class] {
moduleIndex++
physicalGenDir := filepath.Join(targetGenDir, moduleInstance.Directory)
prettyDir, _ := filepath.Rel(baseDirectory, physicalGenDir)
PrintGenLine(moduleInstance.ClassType, moduleInstance.ClassName, moduleInstance.InstanceName, prettyDir, moduleIndex, moduleCount)
err := system.Build(packageRoot, baseDirectory, physicalGenDir, moduleInstance, options)
if err != nil {
return nil, err
}
}
}
for i, hook := range system.postGenHook {
if err := hook(resolvedModules); err != nil {
return resolvedModules, errors.Wrapf(err, "error running %dth post generation hook", i)
}
}
return resolvedModules, nil
}
// PrintGenLine prints the module generation process to stdout
func PrintGenLine(
classType, className, instanceName, buildPath string,
idx, count int,
) {
fmt.Printf(
"Generating %12s %12s %-30s in %-50s %d/%d\n",
classType,
className,
instanceName,
buildPath,
idx,
count,
)
}
// FormatGoFile reformat the go file imports
func FormatGoFile(filePath string) error {
goimportsCmd := exec.Command("goimports", "-w", "-e", filePath)
goimportsCmd.Stdout = os.Stdout
goimportsCmd.Stderr = os.Stderr
if err := goimportsCmd.Run(); err != nil {
return errors.Wrapf(err, "failed to goimports file: %q", filePath)
}
return nil
}
// ModuleClass defines a module class in the build configuration directory.
// THis could be something like an Endpoint class which contains multiple
// endpoint configurations, or a Lib class, that is itself a module instance
type ModuleClass struct {
Name string
NamePlural string
ClassType moduleClassType
DependsOn []string
DependedBy []string
types map[string]BuildGenerator
// private field which is populated before module resolving
dependentClasses []*ModuleClass
}
// BuildResult is the result of running a module generator
type BuildResult struct {
// Files contains a map of file names to file bytes to be written to the
// module build directory
Files map[string][]byte
// Spec is an arbitrary type that can be used to share computed data
// between dependencies
Spec interface{}
}
// BuildGenerator provides a function to generate a module instance build
// artifact from its configuration as part of a build step. For example, an
// Endpoint module instance may generate endpoint handler code
type BuildGenerator interface {
Generate(
instance *ModuleInstance,
) (*BuildResult, error)
}
// SpecProvider is a generator that can provide a specification without
// running the build step.
type SpecProvider interface {
ComputeSpec(
instance *ModuleInstance,
) (interface{}, error)
}
// PackageInfo provides information about the package associated with a module
// instance.
type PackageInfo struct {
// PackageName is the name of the generated package, and should be the same
// as the package name used by any custom code in the config directory
PackageName string
// PackageAlias is the unique import alias for non-generated packages
PackageAlias string
// PackageRoot is the unique import root for non-generated packages
PackageRoot string
// GeneratedPackageAlias is the unique import alias for generated packages
GeneratedPackageAlias string
// GeneratedModulePackageAlias is the unique import alias for the module system's,
// generated subpackage
GeneratedModulePackageAlias string
// PackagePath is the full package path for the non-generated code
PackagePath string
// GeneratedPackagePath is the full package path for the generated code
GeneratedPackagePath string
// GeneratedModulePackagePath is the full package path for the generated dependency
// structs and initializers
GeneratedModulePackagePath string
// QualifiedInstanceName for this package. Pascal case name for this module.
QualifiedInstanceName string
// ExportName is the name on the module initializer function
ExportName string
// ExportType refers to the type returned by the module initializer
ExportType string
// InitializerName is the name of function that can fully initialize the
// module and its dependencies
InitializerName string
// IsExportGenerated is true if the export type is provided by the
// generated package, otherwise it is assumed that the export type resides
// in the non-generated package
IsExportGenerated bool
// CustomInitialisation if CustomInitialisation is true it basically changes the
// import path of the module. For e.g. if the import path of a module is
// package import path: "github.com/uber/zanzibar/examples/example-gateway/build/clients/echo"
// module import path: "github.com/uber/zanzibar/examples/example-gateway/build/clients/echo/module"
// then with CustomInitialisation=true the new import path are
// package import path: "github.com/uber/zanzibar/examples/example-gateway/clients/echo"
// module import path: "github.com/uber/zanzibar/examples/example-gateway/clients/echo"
// hence the new import path are outside of the build folder or generated code, giving developer control over
// the constructors of the module.
CustomInitialisation bool
}
// ImportPackagePath returns the correct package path for the module's exported
// type, depending on which package (generated or not) the type lives in
func (info *PackageInfo) ImportPackagePath() string {
if info.CustomInitialisation {
return info.PackagePath
}
if info.IsExportGenerated {
return info.GeneratedPackagePath
}
return info.PackagePath
}
// ImportPackageAlias returns the correct package alias for referencing the
// module's exported type, depending on whether or not the export is generated
func (info *PackageInfo) ImportPackageAlias() string {
if info.CustomInitialisation {
return info.PackageAlias
}
if info.IsExportGenerated {
return info.GeneratedPackageAlias
}
return info.PackageAlias
}
// ModulePackagePath returns the correct package path for the module's exported
// type, depending on which package (generated or not) the type lives in
func (info *PackageInfo) ModulePackagePath() string {
if info.CustomInitialisation {
return info.PackagePath
}
return info.GeneratedModulePackagePath
}
// ModulePackageAlias returns the correct package path for the module's exported
// type, depending on which package (generated or not) the type lives in
func (info *PackageInfo) ModulePackageAlias() string {
if info.CustomInitialisation {
return info.PackageAlias
}
return info.GeneratedModulePackageAlias
}
// ModuleInstance is a configured module inside a module class directory.
// For example, this could be
// ClassName: "Endpoint,
// ClassType: "http",
// BaseDirectory "/path/to/service/base/"
// Directory: "clients/health/"
// InstanceName: "health",
type ModuleInstance struct {
// genSpec is used to share generated specs across dependencies. Generators
// should not mutate this directly, and should return the spec as a result.
// Only the module system code should mutate a module instance.
genSpec interface{} // protected by mu
// PackageInfo is the name for the generated module instance
PackageInfo *PackageInfo
// ClassName is the name of the class as defined in the module system
ClassName string
// ClassType is the type of the class as defined in the module system
ClassType string
// BaseDirectory is the absolute path to module system system top level
// directory
BaseDirectory string
// Directory is the relative instance directory
Directory string
// InstanceName is the name of the instance as configured in the instance's
// YAML/JSON file
InstanceName string
// Config is a reference to the instance "config" key in the instances YAML
// file.
Config map[string]interface{}
// Dependencies is a list of dependent modules as defined in the instances
// YAML file
Dependencies []ModuleDependency
// Resolved dependencies is a list of direct dependent modules after processing
// (fully resolved)
ResolvedDependencies map[string][]*ModuleInstance
// Recursive dependencies is a list of dependent modules and all of their
// dependencies, i.e. the full tree of dependencies for this module. Each
// class list is sorted for initialization order
RecursiveDependencies map[string][]*ModuleInstance
// DependencyOrder is the bottom to top order in which the recursively
// resolved dependency class names can depend on each other
DependencyOrder []string
// The JSONFileName is file name of the instance JSON file
JSONFileName string // Deprecated
// The YAMLFileName is file name of the instance YAML file
YAMLFileName string
// JSONFileRaw is the raw JSON file read as bytes used for future parsing
JSONFileRaw []byte // Deprecated
// YAMLFileRaw is the raw YAML file read as bytes used for future parsing
YAMLFileRaw []byte
// SelectiveBuilding allows the module to be built with subset of dependencies
SelectiveBuilding bool
mu sync.RWMutex
// QPSLevels is map of circuit breaker name to qps level for all circuit breakers
QPSLevels map[string]int
// CustomTemplates if using custom templates. we can write collection of custom templates that can be passed to zanzibar
CustomTemplates *Template
}
func (instance *ModuleInstance) String() string {
return fmt.Sprintf("[instance %q %q]", instance.ClassType, instance.InstanceName)
}
// GeneratedSpec returns the last spec result returned for the module instance
func (instance *ModuleInstance) GeneratedSpec() interface{} {
instance.mu.RLock()
defer instance.mu.RUnlock()
return instance.genSpec
}
// Equal checks equality of two ModuleInstances
func (instance *ModuleInstance) equal(other *ModuleInstance) bool {
return instance.InstanceName == other.InstanceName && instance.ClassName == other.ClassName
}
// AsModuleDependency creates an equivalent ModuleDependency object for this instance
func (instance *ModuleInstance) AsModuleDependency() ModuleDependency {
return ModuleDependency{
InstanceName: instance.InstanceName,
ClassName: instance.ClassName,
}
}
// ModuleDependency defines a module instance required by another instance
type ModuleDependency struct {
// ClassName is the name of the class as defined in the module system
ClassName string
// InstanceName is the name of the dependency instance as configu
InstanceName string
}
func (m ModuleDependency) equal(other *ModuleInstance) bool {
return m.InstanceName == other.InstanceName && m.ClassName == other.ClassName
}
// ClassConfigBase defines the shared data fields for all class configs.
// Configs of different classes can derive from this base struct by add a
// specific "config" field and "dependencies" field
type ClassConfigBase struct {
// Name is the class instance name used to identify the module as a
// dependency. The combination of the class Name and this instance name
// is unique.
Name string `yaml:"name" json:"name"`
// Type refers to the class type used to generate the dependency
Type string `yaml:"type" json:"type"`
// IsExportGenerated determines whether or not the export lives in
// IsExportGenerated defaults to true if not set.
IsExportGenerated *bool `yaml:"IsExportGenerated,omitempty" json:"IsExportGenerated"`
// CustomInitialisation defaults to false if not set.
CustomInitialisation *bool `yaml:"CustomInitialisation,omitempty" json:"CustomInitialisation"`
// Owner is the Name of the class instance owner
Owner string `yaml:"owner,omitempty"`
// SelectiveBuilding allows the module to be built with subset of dependencies
SelectiveBuilding bool `yaml:"selectiveBuilding,omitempty" json:"selectiveBuilding"`
}
// ClassConfig maps onto a YAML configuration for a class type
type ClassConfig struct {
ClassConfigBase `yaml:",inline" json:",inline"`
// Dependencies is a map of class name to a list of instance names. This
// infers the dependencies struct generated for the initializer
Dependencies map[string][]string `yaml:"dependencies" json:"dependencies"`
// The configuration map for this class instance. This depends on the
// class name and class type, and is interpreted by each module generator.
Config map[string]interface{} `yaml:"config" json:"config"`
}
// NewClassConfig unmarshals raw bytes into a ClassConfig struct
// or return an error if it cannot be unmarshaled into the struct
func NewClassConfig(raw []byte) (*ClassConfig, error) {
config := &ClassConfig{}
parseErr := yaml.Unmarshal(raw, config)
if parseErr != nil {
return nil, errors.Wrapf(
parseErr,
"Error yaml parsing config",
)
}
if config.Name == "" {
return nil, errors.Errorf(
"Error reading instance name",
)
}
if config.Type == "" {
return nil, errors.Errorf(
"Error reading instance type",
)
}
if config.Dependencies == nil {
config.Dependencies = map[string][]string{}
}
return config, nil
}
// writeFile is like ioutil.WriteFile with a mkdirp step
func writeFile(filePath string, bytes []byte) error {
if _, err := os.Stat(filePath); os.IsNotExist(err) {
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
return errors.Wrapf(
err, "could not make directory: %q", filePath,
)
}
}
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
defer closeFile(file)
if err != nil {
return errors.Wrapf(
err, "Could not open file for writing: %q", filePath,
)
}
n, err := file.Write(bytes)
if err != nil {
return errors.Wrapf(err, "Error writing to file %q", filePath)
}
if n != len(bytes) {
return errors.Wrapf(
err,
"Error writing full contents to file: %q",
filePath,
)
}
return nil
}
func closeFile(file *os.File) {
_ = file.Close()
}