美文网首页
CWL 的 Go 语言解析

CWL 的 Go 语言解析

作者: tongesysu | 来源:发表于2022-08-17 11:49 被阅读0次

cwl 是一种工作流描述语言,官方提供了系列工具,但是是 python 版本的

由于个人项目主要是基于GO进行开发,现对 Go 运行 CWL 的现状做一二说明:

实现列表

项目目前使用的基础,支持语言的解析和运行,也提供了测试,但完成度较低:

  • 尚不支持 Workflow,不支持 ExpressionTool, 只支持 CommandLineTool

  • CommandLineTool 的部分支持存在问题(26/64):

    • 不支持 SchemaDefRequirement
    • 对 InlineJavascriptRequirement 的支持不够完善
    • 不支持 STDIN 等
    • 对 Any Type 的支持不够完善
    • 对 initialWorkDir 的支持不够完善
    • 对 shellQuote 的支持不够完善
    • 对 successCode 的支持不够完善
    • ... ...
  • otiai10 cwl.go

一个 CWL 的 解析器,没有执行器,对应版本为 version 1.0 , 提供了解析测试

一个CWL K8s 执行器,其解析部分基于 otiai10 forked 版本进行开发(但不通过 otiai10 的测试),提供了测试工具,但未报道通过率

CWL 特点

CWL 语法非常灵活,例如:

  • slice , Map 等效写法

CWL 的设计比较灵活,为了方便手写,同一表达可以有多种方式,例如:

inputs:
  - id: x
    type: File
inputs:
  x:
    type: File
inputs:
  x: File

均是合法的表达,导致了在静态语言中进行解析的困难。

  • 特殊缩写法 例如,简写
[{
  "extype": "string"
}, {
  "extype": "string?"
}, {
  "extype": "string[]"
}, {
  "extype": "string[]?"
}]

在经过 salad 预处理后,可以展开为:

    {
        "extype": "string"
    }, 
    {
        "extype": [
            "null", 
            "string"
        ]
    }, 
    {
        "extype": {
            "type": "array", 
            "items": "string"
        }
    }, 
    {
        "extype": [
            "null", 
            {
                "type": "array", 
                "items": "string"
            }
        ]
    }
]

类似的通过预处理规则来简化CWL书写的地方还有很多,但也提高了解析难度。

实际,CWL标准特别定义了一种 JSON 扩展 ,可以通过 salad预处理 来处理 CWL JSON 的验证和解析

但由于尚未有 salad 的 GO 语言实现,目前大部分实现都是直接进行 CWL 的 解析

GO 语言中的解析

  • 基于 map[string]interface{} 的逐层解析法:

otiai10 中的处理方式是,定义好数据结构,通过 interface{} 逐层解析:

// Root ...
type Root struct {
    Version      string
    Class        string
    Hints        Hints
    Doc          string
    Graphs       Graphs
    BaseCommands BaseCommands
    Arguments    Arguments
    Namespaces   Namespaces
    Schemas      Schemas
    Stdin        string
    Stdout       string
    Stderr       string
    Inputs       Inputs `json:"inputs"`
    // ProvidedInputs ProvidedInputs `json:"-"`
    Outputs      Outputs
    Requirements Requirements
    Steps        Steps
    ID           string // ID only appears if this Root is a step in "steps"
    Expression   string // appears only if Class is "ExpressionTool"

    // Path
    Path string `json:"-"`
    // InputsVM
    InputsVM *otto.Otto
}

// UnmarshalJSON ...
func (root *Root) UnmarshalJSON(b []byte) error {
    docs := map[string]interface{}{}
    if err := json.Unmarshal(b, &docs); err != nil {
        return err
    }
    return root.UnmarshalMap(docs)
}

// UnmarshalMap decode map[string]interface{} to *Root.
func (root *Root) UnmarshalMap(docs map[string]interface{}) error {
    for key, val := range docs {
        switch key {
        case "cwlVersion":
            root.Version = val.(string)
        case "class":
            root.Class = val.(string)
        case "hints":
            root.Hints = root.Hints.New(val)
        case "doc":
            root.Doc = val.(string)
        case "baseCommand":
            root.BaseCommands = root.BaseCommands.New(val)
        ...
// New constructs new "Inputs" struct.
func (ins Inputs) New(i interface{}) Inputs {
    dest := Inputs{}
    switch x := i.(type) {
    case []interface{}:
        for _, v := range x {
            dest = append(dest, Input{}.New(v))
        }
    case map[string]interface{}:
        for key, v := range x {
            input := Input{}.New(v)
            input.ID = key
            dest = append(dest, input)
        }
    }
    return dest
}
...

省略说明了 通过 yaml2json 将 CWL YAML 转换为 JSON 的过程
通过定义每一层数据结构的 New(i interface{}) 函数来进行解析的处理

一些字段有多个格式时,则通过组合构造结构进行处理:

// Requirement represent an element of "requirements".
type Requirement struct {
    Class string
    InlineJavascriptRequirement
    SchemaDefRequirement
    DockerRequirement
    SoftwareRequirement
    InitialWorkDirRequirement
    EnvVarRequirement
    ShellCommandRequirement
    ResourceRequirement
    Import string
}
func (_ Requirement) New(i interface{}) Requirement {
    dest := Requirement{}
    switch x := i.(type) {
    case map[string]interface{}:
        for key, v := range x {
            switch key {
            case "class":
                dest.Class = v.(string)
            case "coresMin":
                fmt.Println(v)
                dest.CoresMin = int(v.(float64))
            case "coresMax":
                fmt.Println(v)
                dest.CoresMax = int(v.(float64))
            case "ramMin":
                fmt.Println(v)
                dest.RAMMin = int(v.(float64))
            case "ramMax":
                dest.RAMMax = int(v.(float64))
            case "dockerPull":
                dest.DockerPull = v.(string)
...

这种方式看起来逻辑比较清晰,数据结构需要合理设计,解析过程需要按设计的规则推进

  • 基于 reflect 的通用化解析法

buchanae 采用了另一种解析方式,通过反射的方式来进行解析:


func LoadDocumentBytes(b []byte, base string, r Resolver) (Document, error) {
...

    l := loader{base, r}
    // Parse the YAML into an AST
    yamlnode, err := yamlast.Parse(b)
...
    // Being recursively processing the tree.
    var d Document
    start := node(yamlnode.Children[0])
    start, err = l.preprocess(start)
    if err != nil {
        return nil, err
    }

    err = l.load(start, &d)
    if err != nil {
        return nil, err
    }
    if d != nil {
        return d, nil
    }
    return nil, nil
}

yamlast.Parse 将文档解析为 Node AST ,

// Node represents a node within the AST.
type Node struct {
    Kind         int
    Line, Column int
    Tag          string
    Value        string
    Implicit     bool
    Children     []*Node
    Anchors      map[string]*Node
}

l.load(n node, t interface{}) 则 会根据 Node 的类型 (Mapping/Seq/Scalar)和 t 的反射类型 以及元素类型,调用不同的处理函数

// describes the type conversion being requested,
    // in order to look up a registered handler.
    typename := strings.Title(typ.Name())
    if typ.Kind() == reflect.Slice {
        typename = strings.Title(typ.Elem().Name())
        typename += "Slice"
    }
    if typ.Kind() == reflect.Map {
        typename = strings.Title(typ.Elem().Name())
        typename += "Map"
    }
    handlerName := nodeKind + "To" + typename

    // look for a handler. if found, use it.
    if _, ok := loaderTyp.MethodByName(handlerName); ok {
        m := loaderVal.MethodByName(handlerName)
        nval := reflect.ValueOf(n)
        outv := m.Call([]reflect.Value{nval})
        errv := outv[1]
        if !errv.IsNil() {
            return errv.Interface().(error)
        }
        resv := outv[0]
        val.Set(resv)
        return nil
    }
...

通过复杂的函数,将解析请求转发到了不同的解析函数中,例如 MappingToRequirement, MappingToRequirementSlice, SeqToRequirementSlice 等等,再通过特定的 loadReqByName 从 特定字段获取到具体的数据类型进行解析


func (l *loader) MappingToRequirement(n node) (Requirement, error) {
    class := findKey(n, "class")
    return l.loadReqByName(class, n)
}

func (l *loader) loadReqByName(name string, n node) (Requirement, error) {
    switch strings.ToLower(name) {
    case "dockerrequirement":
        d := DockerRequirement{}
        err := l.load(n, &d)
        return d, err
    case "resourcerequirement":
        r := ResourceRequirement{}
        err := l.load(n, &r)
        return r, err
    case "envvarrequirement":
        r := EnvVarRequirement{}
        err := l.load(n, &r)
        return r, err
    case "shellcommandrequirement":
        s := ShellCommandRequirement{}
        err := l.load(n, &s)
        return s, err
    case "inlinejavascriptrequirement":
        j := InlineJavascriptRequirement{}
        err := l.load(n, &j)
        return j, err
    case "schemadefrequirement":
        r := SchemaDefRequirement{}
        err := l.load(n, &r)
        return r, err
    case "softwarerequirement":
        r := SoftwareRequirement{}
        err := l.load(n, &r)
        return r, err
    case "initialworkdirrequirement":
        r := InitialWorkDirRequirement{}
        err := l.load(n, &r)
        return r, err
    case "subworkflowfeaturerequirement":
        return SubworkflowFeatureRequirement{}, nil
    case "scatterfeaturerequirement":
        return ScatterFeatureRequirement{}, nil
    case "multipleinputfeaturerequirement":
        return MultipleInputFeatureRequirement{}, nil
    case "stepinputexpressionrequirement":
        return StepInputExpressionRequirement{}, nil
    }
    return UnknownRequirement{Name: name}, nil
    // TODO logging
    //return nil, fmt.Errorf("unknown requirement name: %s", name)
}

从中可以看出, 这个方案虽然使用了GO的很多高级特性,但并未使解析过程简单,过度抽象反而使其更加的难以理解和开发。

相应的也有好处,比如在对语言做扩展时,例如扩展 Requirement / Hit 时,只需要添加一个对应接口的实现,处理方式比较符合 GO 语言的设计模式

Better Parser ?

实现一个 Go Schema_SALAD 预处理器,将 CWL JSON 转化为 standard JSON 可能是一个更加合适的方案?

相关文章

网友评论

      本文标题:CWL 的 Go 语言解析

      本文链接:https://www.haomeiwen.com/subject/roorgrtx.html