CWL 的 Go 语言解析

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 可能是一个更加合适的方案?

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351

推荐阅读更多精彩内容