2024-02-05更新。

草稿

填写模板

在这个Pkl教程的第二部分中,你将学习如何用另一个配置来表达一个(部分的)配置。你还将找到并填写一个现有的模板。

编写配置

修订

Pkl中用于表达一个配置(或配置的一部分)相对于另一个配置的中心机制是修改。考虑以下示例。amendingObjects.pkl

笔记
bird { name = "Pigeon" diet = "Seeds" taxonomy { kingdom = "Animalia" clade = "Dinosauria" order = "Columbiformes" } } parrot = (bird) { name = "Parrot" diet = "Berries" taxonomy { order = "Psittaciformes" } }

鹦鹉和鸽子具有几乎相同的特性。它们唯一的区别在于它们的名称和分类学,所以如果你已经写出了鸟类,你可以说鹦鹉就像鸽子一样,除了名字是“鹦鹉”,食物是“浆果”,分类学上的目是“鹦形目”。当你运行这个,Pkl会完全展开一切。

笔记
bird { name = "Common wood pigeon" diet = "Seeds" taxonomy { kingdom = "Animalia" clade = "Dinosauria" order = "Columbiformes" } 1 } parrot { name = "Parrot" diet = "Berries" taxonomy { kingdom = "Animalia" clade = "Dinosauria" order = "Psittaciformes" } }

修正不允许我们向我们正在修改的(类型化的)对象添加属性。教程的下一部分将更详细地讨论类型。在那里,你会看到修改从不改变对象的类型。你也可以修改嵌套的对象。这使你只需要描述与最外层对象的区别,就可以针对任意深层嵌套的结构。考虑以下例子。nestedAmends.pkl

笔记
woodPigeon { name = "Common wood pigeon" diet = "Seeds" taxonomy { species = "Columba palumbus" } } stockPigeon = (woodPigeon) { name = "Stock pigeon" taxonomy { ① species = "Columba oenas" } } dodo = (stockPigeon) { ② name = "Dodo" extinct = true ③ taxonomy { species = "Raphus cucullatus" } }

① 这修正了种(species)的概念,正如它在原生鸽(stockPigeon)中出现的那样。② 修正的对象本身,也可以被修正。③ 在修正对象时可以添加新的字段。注意到你只需要改变taxonomy.species。在此示例中,bird.taxonomy 包含了界(kingdom)、演化支(clade)、目(order)和种(species)。你正在修正原生鸽(stockPigeon),以定义林鸽(woodPigeon)。它们的分类学(taxonomy)前两者相同,除了种(species)。这种记法说明在分类学(taxonomy)中的所有内容应该与你正在修正的对象(originalPigeon)中的相同,除了种(species),应该是“Columba palumbus”。针对上述输入,Pkl产生了以下输出。

笔记
woodPigeon { name = "Common wood pigeon" diet = "Seeds" taxonomy { species = "Columba palumbus" } } stockPigeon { name = "Stock pigeon" diet = "Seeds" taxonomy { species = "Columba oenas" } } dodo { name = "Dodo" diet = "Seeds" extinct = true taxonomy { species = "Raphus cucullatus" } }

到目前为止,你只修改了属性。由于你是通过名称来引用它们,因此你从被修改对象中“覆写”值是合理的。如果你在修改表达式中包括了元素或条目会怎样呢?amendElementsAndEntries.pkl

笔记
favoriteFoods { "red berries" "blue berries" ["Barn owl"] { "mice" } } adultBirdFoods = (favoriteFoods) { [1] = "pebbles" ① "worms" ② ["Falcon"] { ③ "insects" "amphibians" } ["Barn owl"] { ④ 3 "fish" } }

① 明确通过索引修改会替换该索引处的元素。 ② 如果没有明确的索引,Pkl无法知道要覆盖哪个元素,因此,它会向你要修改的对象中添加一个元素。 ③ 当你写入“新”的条目(使用在你要修改的对象中不存在的键)时,Pkl也会添加它们。 ④ 当你使用已存在的键写入条目时,该记法修正了其值。Pkl无法仅通过它们的值知道要覆盖哪个favoriteFoods。当你想替换一个元素时,你必须明确地在特定索引处修改元素。这就是为什么在修改表达式中的一个“普通”元素会被添加到正在修改的对象中。结果:

笔记
favoriteFoods { ["Barn owl"] { "mice" } "red berries" "blue berries" } adultBirdFoods { ["Barn owl"] { "mice" "fish" } "red berries" "pebbles" ["Falcon"] { "insects" "amphibians" } "worms" }

模块

pkl 文件描述了一个模块。模块是可以从其他模块中引用的对象。回到上面的例子,你可以将 parrot 写为一个独立的模块。pigeon.pkl
笔记
name = "Common wood pigeon" diet = "Seeds" taxonomy { species = "Columba palumbus" 4 }

你可以导入这个模块,并像之前一样表达parrot。parrot.pkl

笔记
import "pigeon.pkl" ① parrot = (pigeon) { name = "Great green macaw" diet = "Berries" species { species = "Ara ambiguus" } }

导入foo.pkl会创建对象foo,所以你可以在这段代码中引用pigeon,就像之前做的那样。如果你在两者上都运行Pkl,你会看到它是起作用的。但是,观察结果时,你会看到一个(可能)出乎意料的差异。

笔记
$ pkl eval /Users/me/tutorial/pigeon.pkl name = "Common wood pigeon"" diet = "Seeds" taxonomy { species = "Columba palumbus" } $ pkl eval /Users/me/tutorial/parrot.pkl parrot { name = "Great green macaw" diet = "Berries" taxonomy { species = "Ara ambiguus" } }

这个对象鸽子在顶层被“展开”,而鹦鹉是一个嵌套的命名对象。这是因为编写鹦鹉 {…​} 定义了“当前”模块中的一个对象属性。为了表达“这个模块是一个对象,来自于鸽子模块的修改版”,你需要使用一个修改条款。parrot.pkl

笔记
amends "pigeon.pkl" ① 5 name = "Great green macaw"

① 这个模块与“pigeon.pkl”相同,除了文件剩余部分的内容。注意,作为第一直觉,可以把“修订一个模块”想象成“填写一份表格”。

修订模板

一个Pkl文件可以是一个模板或者一个“普通”的模块。这一术语描述了模块的预期用途,并不意味着关于其结构的任何事情。换句话说:仅仅通过查看Pkl代码,你是无法判断它是一个模板还是一个“普通”的模块。acmecicd.pkl

笔记
module acmecicd class Pipeline { name: String(nameRequiresBranchName)? hidden nameRequiresBranchName = (_) -> if (branchName == null) throw("Pipelines that set a 'name' must also set a 'branchName'.") else true branchName: String? } timeout: Int(this >= 3) pipelines: Listing<Pipeline> output { renderer = new YamlRenderer {} }

请记住,修订就像填写表格一样。这正是你在这里做的;你在填写“工作订单表”。接下来,为你的工作添加一分钟的超时。cicd.pkl

笔记
amends "acmecicd.pkl" timeout = 1

不幸的是,Pkl不接受这种配置,并提供了一个相当详细的错误信息:

笔记
–– Pkl Error –– ① Type constraint `this >= 3` violated. ② Value: 1 ③ 225 | timeout: Int(this >= 3)? ④ ^^^^^^^^^ at acmecicd#timeout (file:///Users/me/tutorial/acmecicd.pkl, line 8) 3 | timeout = 1 ⑤ ^ at cicd#timeout (file:///Users/me/tutorial/cicd.pkl, line 3) 90 | text = renderer.renderDocument(value) ⑥ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ at pkl.base#Module.output.text (https://github.com/apple/pkl/blob/e4d8c882d/stdlib/base.pkl#L90)

① Pkl发现了一个错误。② Pkl发现了哪个错误。③ 什么是冒犯性的值。④ Pkl在哪里找到它的期望值(修订后的模块的第8行)。⑤ Pkl在哪里找到了冒犯性的值(输入模块的第3行)。⑥ Pkl评估什么来发现错误。当Pkl打印源位置时,它还会打印可点击的链接,便于访问。对于本地文件,它会为你的开发环境生成一个链接(可以在 ~/.pkl/settings.pkl 中配置)。对于从其他地方导入的包,如果可用,Pkl会生成指向其存储库的https://链接。Pkl对类型约束提出了抱怨。Pkl的类型系统不仅能防止你在期望Int的地方提供一个String,甚至还会检查哪些值是被允许的。在这个案例中,最小超时时间为三分钟。如果你将值更改为3,则Pkl接受你的配置。

笔记
$ pkl eval cicd.pkl timeout: 3 pipelines: []

你现在可以定义一个流水线。首先指定流水线的名称,其他暂时不用添加。cicd.pkl

笔记
amends "acmecicd.pkl" timeout = 3 pipelines { new { ① name = "prb" } 7 }

① 没有现成的管道对象可供修改。new关键字提供了一个可供修改的对象。到目前为止,你定义对象的方式与修改它们的方式相同。当名称foo之前未出现过时,foo { … } 会创建一个名为foo的属性,并将指定于…​上的对象分配给它。如果foo是一个现有对象,这种表示法是一种修改表达式;它会产出一个新的对象(值),但不会产出一个新的(命名的)属性。由于pipelines是一个列表,你可以通过在修改表达式中编写表达式来添加元素。然而,在这种情况下,没有对象可以修改。编写myNewPipeline { … } 定义了一个属性,但列表可能只包含元素。这就是你可以使用new关键字的地方。new提供了一个可供修改的对象。Pkl来自于new被使用的上下文以及应该修改什么样的对象。这被称为上下文的默认值。下一部分将详细介绍Pkl如何做到这一点。在你的新配置上运行Pkl会产生一个冗长的错误信息。cicd.pkl

笔记
–– Pkl Error –– Pipelines that set a 'name' must also set a 'branchName'. 8 | throw("Pipelines that set a 'name' must also set a 'branchName'.") ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ at acmecicd#Pipeline.nameRequiresBranchName.<function#1> (file:///Users/me/tutorial/acmecicd.pkl, line 8) 6 | name = "prb" ^^^^^ at cicd#pipelines[#1].name (file:///Users/me/tutorial/cicd.pkl, line 6) 90 | text = renderer.renderDocument(value) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ at pkl.base#Module.output.text (https://github.com/apple/pkl/blob/e4d8c882d/stdlib/base.pkl#L90)

您之前遇到了另一种类型的约束,例如超时:Int(这个>=3)。在这种情况下,错误信息是由一句英语句子组成的,而不是Pkl代码。当约束条件复杂或非常具体到应用时,模板作者可以抛出更具描述性的错误信息,就像这样。该信息非常有指导意义,所以您可以通过添加一个分支名称来修正错误。cicd.pkl

笔记
amends "acmecicd.pkl" timeout = 3 8 pipelines { new { name = "prb" branchName = "main" } }

确实如此

笔记
$ pkl eval -f yml /Users/me/tutorial/cicd.pkl timeout: 3 pipelines: - name: prb branchName: main