This guide is intended for maintainers.
All development targets the main branch. New releases for the latest major version series are cut from this branch.
For the older major versions, we also have release-<major>.x branches where we try to backport all bug fixes.
Backports are done by tibdex/backport. Apply the label backport release-<major>.x to the PRs and once they are merged a new PR is opened.
- Generally, pointers are used whenever a variable's value is nullable.
- As few pointers as possible should be used to avoid nil dereference errors.
- Leverage Go empty values where possible, e.g. if a value is required, so an empty value has no other meaning.
- Sometimes the API treats an empty value as
nil, for example an empty string in name updates. There no pointer should be used.
type ExampleUpdateRequest struct {
// Name is optional but cannot be empty.
// It doesn't need to be a pointer, we can represent empty values with "".
Name string `json:"name,omitempty"`
// Description is optional and can be empty.
// We need a pointer to distinguish between an empty value and no value.
Description *string `json:"description,omitempty"`
// Foo is required, it should not be a pointer and should not be omitted.
Foo int `json:"foo"`
// Bar is required but nullable. It should be a pointer.
Bar *int `json:"bar,omitempty"`
}- We use the
schemapackage to map JSON schemas from the API to Go structs as closely as possible. - Structs that fulfill some kind of function should be implemented in the
hcloudpackage.- Structs can be converted to/from schemas using code generated by the
govertertool. (See schema_gen.go) - Conversion between structs should be possible without loss of information.
- Structs can be converted to/from schemas using code generated by the
Warning
Since for maps and slices nil will serialize to {} or [] respectively, in the schema package optional maps/slices should
be a pointer. In the hcloud package they should not be, because nil maps/slices can be mapped to a nil pointer during conversion.
- If an action endpoint returns more than just an action, a
<...>Resultstruct should be used.- This struct must not be returned as a pointer.
- If an error occurs, an empty struct should be returned.
- Alternatively, only the action can be returned.
- This should be done only if it is certain that in the future no additional values will be returned, as this would be a breaking change.
- We only do basic validation on the client side. Business logic is validated on the API side.
- error.go contains helpers for validation. Validation should be implemented as a
Validate()function on theOptsstruct.
// Validate checks if options are valid.
func (o ExampleOpts) Validate() error {
if o.Foo == "" {
return missingField(o, "Foo")
}
if o.Bar == nil && o.Baz == nil {
return missingOneOfFields(o, "Bar", "Baz")
}
if o.Qux <= 0 {
return invalidFieldValue(o, "Qux", o.Qux)
}
return nil
}- A subresource should contain a reference to its parent that can be used to address endpoints. This reduces duplication.
- Example:
- Correct:
func UpdateSubaccount(context.Context, *hcloud.StorageBoxSubaccount, StorageBoxSubaccountUpdateOpts) - Wrong:
func UpdateSubaccount(context.Context, *hcloud.StorageBox, *hcloud.StorageBoxSubaccount, StorageBoxSubaccountUpdateOpts)
- Correct: