d20ffbe8be
O.1 docs is deprecated. Let's inform user to switch the the latest version Signed-off-by: user.email <jf@dagger.io>
384 lines
6.6 KiB
Markdown
384 lines
6.6 KiB
Markdown
---
|
||
slug: /1226/coding-style
|
||
displayed_sidebar: '0.2'
|
||
---
|
||
|
||
# Package Coding Style
|
||
|
||
Please follow these guidelines when contributing CUE packages to keep consistency,
|
||
improve clarity and avoid issues.
|
||
|
||
## Public names
|
||
|
||
Choose `PascalCase` for "public" definitions (importable outside of package).
|
||
|
||
```cue
|
||
// good
|
||
#WriteFile: {}
|
||
|
||
// bad
|
||
#writeFile: {}
|
||
#write_file: {}
|
||
```
|
||
|
||
Choose `camelCase` for "public" fields, instead of `snake_case` or `"kebab-case"`.
|
||
|
||
```cue
|
||
// good
|
||
sshKey: ...
|
||
|
||
// bad
|
||
ssh_key: ...
|
||
"ssh-key": ...
|
||
```
|
||
|
||
## Private names
|
||
|
||
Choose `_#camelCase` for private definitions that should only be used in current package.
|
||
|
||
```cue
|
||
// good
|
||
_#mergeStructs: { ... }
|
||
|
||
// bad, starts with lower case but not private
|
||
#mergeStructs: { ... }
|
||
|
||
// bad, starts with upper case but private
|
||
_#MergeStructs: { ... }
|
||
```
|
||
|
||
Choose `_camelCase` for private fields (implementation details).
|
||
|
||
```cue
|
||
#Copy: {
|
||
// good, indicates internal implementation
|
||
_copy: core.#Copy
|
||
|
||
// bad, indicates it can be used directly
|
||
copy: core.#Copy
|
||
}
|
||
```
|
||
|
||
## Definitions for _schemas_, fields for concrete _implementations_
|
||
|
||
```cue
|
||
// good, defines a schema
|
||
#Copy: {
|
||
// good, implements a schema
|
||
input: #Image
|
||
_copy: core.#Copy & { ... }
|
||
}
|
||
|
||
// bad, not a definition
|
||
Copy: {
|
||
// bad, not using a visible or hidden field (according to intent)
|
||
#input: #Image
|
||
_#copy: core.#Copy & { ... }
|
||
}
|
||
|
||
// ok for mixin
|
||
Mixin: { ... }
|
||
copy: #Copy & Mixin & { ... }
|
||
```
|
||
|
||
## All public definitions and fields must be documented
|
||
|
||
Documenting other fields is optional. The main idea is that public fields should be documented for someone
|
||
reading Dagger’s documentation in the browser. Everything else is for someone looking at the code.
|
||
|
||
```cue
|
||
// Copy files into an image
|
||
#Copy: {
|
||
// Image to receive copied files
|
||
input: #Image
|
||
|
||
// Files to copy
|
||
contents: dagger.#FS
|
||
|
||
_copy: ...
|
||
|
||
// Resulting image with added files
|
||
output: ...
|
||
}
|
||
```
|
||
|
||
## One definition per file
|
||
|
||
Split action definitions into their own file as much as possible (unless very tightly related).
|
||
|
||
```cue
|
||
// copy.cue
|
||
#Copy: ...
|
||
|
||
// push.cue
|
||
#Push: ...
|
||
|
||
// exec.cue
|
||
#Exec: ...
|
||
#Mount: ...
|
||
#CacheDir: ...
|
||
```
|
||
|
||
## Use verbs for action names, nouns otherwise
|
||
|
||
Action definition names should be verbs (e.g., `#Build`, `#Run`, `#Push`).
|
||
Other types of definitions should be nouns as much as possible (e.g., `#Image`, `#Secret`, `#Socket`).
|
||
|
||
## No field abbreviations
|
||
|
||
CUE fields are more explicit and intuitive without abbreviations (e.g, `target` or
|
||
`destination` instead of `dest`).
|
||
|
||
## Avoid interpolation
|
||
|
||
There's no need to interpolate a variable if it's already
|
||
a string.
|
||
|
||
```cue
|
||
let greet = "hello world"
|
||
|
||
// bad
|
||
args: ["echo", "\(greet)"]
|
||
read: contents: "\(greet)"
|
||
|
||
// good
|
||
args: ["echo", greet]
|
||
read: contents: greet
|
||
```
|
||
|
||
Even for field names there's a better way:
|
||
|
||
```cue
|
||
// bad
|
||
client: filesystem: "\(path)": read: contents: string
|
||
files: "\(path)": output.contents
|
||
|
||
// good
|
||
client: filesystem: (path): read: contents: string
|
||
files: (path): output.contents
|
||
```
|
||
|
||
It's ok if you need to convert something into a string.
|
||
|
||
```cue
|
||
exit: int
|
||
|
||
env: [string]: string
|
||
env: EXIT: "\(exit)"
|
||
```
|
||
|
||
Interpolating a really short script is actually harmless,
|
||
but not if it discourages from splitting the script into
|
||
its own file.
|
||
|
||
```cue
|
||
// bad
|
||
let name = "world"
|
||
run: bash.#Run & {
|
||
script: contents: "echo hello \(world)"
|
||
}
|
||
|
||
// good
|
||
run: bash.#Run & {
|
||
env: NAME: name
|
||
script: contents: "echo hello $NAME"
|
||
}
|
||
```
|
||
|
||
## Don’t inline scripts
|
||
|
||
Avoid inlining scripts (e.g., _sh_, _py_, etc). Instead, put them in their own files
|
||
with proper extension, and use `core.#Source` to import into CUE. This allows linting
|
||
and avoids some limitations (script size, escaping).
|
||
|
||
Some exceptions may apply (e.g., the script is really short or interpolation is
|
||
actually necessary).
|
||
|
||
```cue
|
||
// bad
|
||
run: bash.#Run & {
|
||
script: contents: """
|
||
...
|
||
...
|
||
...
|
||
"""
|
||
}
|
||
|
||
// good
|
||
src: core.#Source & {
|
||
path: "./src"
|
||
}
|
||
run: bash.#Run & {
|
||
script: {
|
||
directory: src.output
|
||
filename: "setup.sh"
|
||
}
|
||
}
|
||
|
||
// ok
|
||
run: bash.#Run & {
|
||
script: contents: "echo hello world"
|
||
}
|
||
```
|
||
|
||
## Avoid raw strings
|
||
|
||
Don’t use `#"""` unless you actually need it. Most of the time you won't.
|
||
The regular `"""` is simpler to read and less scary for beginners.
|
||
|
||
<!-- TODO: Add example where it actually helps or is ok. -->
|
||
|
||
```cue
|
||
// bad
|
||
run: python.#Run & {
|
||
script: contents: #"""
|
||
print("Hello World!")
|
||
"""#
|
||
}
|
||
|
||
// good
|
||
run: python.#Run & {
|
||
script: contents: """
|
||
print("Hello World!")
|
||
"""
|
||
}
|
||
|
||
// bad
|
||
run: python.#Run & {
|
||
script: contents: #"print("Hello World!")"#
|
||
}
|
||
|
||
// good
|
||
run: python.#Run & {
|
||
script: contents: "print('Hello World')"
|
||
}
|
||
|
||
// good
|
||
run: python.#Run & {
|
||
script: contents: 'print("Hello World")'
|
||
}
|
||
```
|
||
|
||
## Favor disjunctions over _if_ conditions
|
||
|
||
```cue
|
||
// bad
|
||
type: string
|
||
if type == "fs" {
|
||
contents: dagger.#FS
|
||
ro?: true | *false
|
||
}
|
||
if type == "cache" {
|
||
contents: dagger.#Secret
|
||
mask: int
|
||
}
|
||
|
||
// good
|
||
{
|
||
contents: dagger.#FS
|
||
ro?: true | *false
|
||
} | {
|
||
contents: dagger.#Secret
|
||
mask: int
|
||
}
|
||
```
|
||
|
||
## Favor templates over _for_ loops
|
||
|
||
```cue
|
||
// bad
|
||
files: ["stdout.log", "stderr.log"]
|
||
export: {
|
||
for path in files {
|
||
(path): #Read & {
|
||
"path": path
|
||
}
|
||
}
|
||
}
|
||
|
||
// good
|
||
export: [path=string]: #Read & {
|
||
"path": path
|
||
}
|
||
export: {
|
||
"stdout.log": _
|
||
"stderr.log": _
|
||
}
|
||
```
|
||
|
||
## Scope conditions and loops as locally as possible
|
||
|
||
```cue
|
||
// bad
|
||
if host != _|_ {
|
||
env: HOST: host
|
||
}
|
||
|
||
// good
|
||
env: {
|
||
if host != _|_ {
|
||
HOST: host
|
||
}
|
||
}
|
||
|
||
// bad
|
||
for path, output in _files {
|
||
files: (path): output.contents
|
||
}
|
||
|
||
// good
|
||
files: {
|
||
for path, output in _files {
|
||
(path): output.contents
|
||
}
|
||
}
|
||
```
|
||
|
||
## Use _top_ to match anything
|
||
|
||
From [CUE](https://cuelang.org/docs/references/spec/#values-1):
|
||
|
||
> At the top of the lattice is the single ancestor of all values, called _top_, denoted `_` in CUE. Every value is an instance of top.
|
||
|
||
There's a recurring theme when you have a template and need to create instances from it:
|
||
|
||
```cue
|
||
// bad
|
||
files: [path=string]: {
|
||
"path": path
|
||
...
|
||
}
|
||
|
||
// bad
|
||
files: "/output": {}
|
||
|
||
// good
|
||
files: "/output": _
|
||
|
||
```
|
||
|
||
And also when you need to reference something that's being added implicitly.
|
||
|
||
```cue
|
||
actions: build: go.#Run & {
|
||
// these values are being added implicitly to the plan
|
||
// but we need something to reference
|
||
os: client.platform.os
|
||
arch: client.platform.arch
|
||
...
|
||
}
|
||
|
||
// bad, no need to redefine these fields
|
||
client: platform: {
|
||
os: string
|
||
arch: string
|
||
}
|
||
|
||
// good, we only need the `client` field to exist
|
||
client: _
|
||
|
||
|
||
// ok if it improves understanding
|
||
client: platform: _
|
||
```
|