Skip to content

Commit 0959a5a

Browse files
committed
add jsonpatch library and api module
1 parent a996450 commit 0959a5a

File tree

11 files changed

+905
-2
lines changed

11 files changed

+905
-2
lines changed

Taskfile.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ includes:
55
taskfile: hack/common/Taskfile_library.yaml
66
flatten: true
77
vars:
8-
CODE_DIRS: '{{.ROOT_DIR}}/pkg/...'
8+
NESTED_MODULES: api
9+
CODE_DIRS: '{{.ROOT_DIR}}/pkg/... {{.ROOT_DIR}}/api/...'
910
GENERATE_DOCS_INDEX: "true"

api/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/openmcp-project/controller-utils/api
2+
3+
go 1.24.2

api/jsonpatch/types.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package jsonpatch
2+
3+
import "encoding/json"
4+
5+
// JSONPatch represents a JSON patch operation.
6+
// Technically, a single JSON patch is already a list of patch operations. This type represents a single operation, use JSONPatches for a list of operations instead.
7+
type JSONPatch struct {
8+
// Operation is the operation to perform.
9+
// +kubebuilder:validation:Enum=add;remove;replace;move;copy;test
10+
// +kubebuilder:validation:Required
11+
Operation Operation `json:"op"`
12+
13+
// Path is the path to the target location in the JSON document.
14+
// +kubebuilder:validation:Required
15+
Path string `json:"path"`
16+
17+
// Value is the value to set at the target location.
18+
// Required for add, replace, and test operations.
19+
// +optional
20+
Value *Any `json:"value,omitempty"`
21+
22+
// From is the source location for move and copy operations.
23+
// +optional
24+
From *string `json:"from,omitempty"`
25+
}
26+
27+
// JSONPatches is a list of JSON patch operations.
28+
// This is technically a 'JSON patch' as defined in RFC 6902.
29+
type JSONPatches []JSONPatch
30+
31+
type Operation string
32+
33+
const (
34+
// ADD is the constant for the JSONPatch 'add' operation.
35+
ADD Operation = "add"
36+
// REMOVE is the constant for the JSONPatch 'remove' operation.
37+
REMOVE Operation = "remove"
38+
// REPLACE is the constant for the JSONPatch 'replace' operation.
39+
REPLACE Operation = "replace"
40+
// MOVE is the constant for the JSONPatch 'move' operation.
41+
MOVE Operation = "move"
42+
// COPY is the constant for the JSONPatch 'copy' operation.
43+
COPY Operation = "copy"
44+
// TEST is the constant for the JSONPatch 'test' operation.
45+
TEST Operation = "test"
46+
)
47+
48+
// NewJSONPatch creates a new JSONPatch with the given values.
49+
func NewJSONPatch(op Operation, path string, value *Any, from *string) JSONPatch {
50+
return JSONPatch{
51+
Operation: op,
52+
Path: path,
53+
Value: value,
54+
From: from,
55+
}
56+
}
57+
58+
// NewJSONPatches combines multiple JSONPatch instances into a single JSONPatches instance.
59+
// This is a convenience function to create a JSONPatches instance from multiple JSONPatch instances.
60+
func NewJSONPatches(patches ...JSONPatch) JSONPatches {
61+
result := make(JSONPatches, 0, len(patches))
62+
for _, patch := range patches {
63+
result = append(result, patch)
64+
}
65+
return result
66+
}
67+
68+
func NewAny(value any) *Any {
69+
return &Any{Value: value}
70+
}
71+
72+
type Any struct {
73+
Value any `json:"-"`
74+
}
75+
76+
var _ json.Marshaler = &Any{}
77+
var _ json.Unmarshaler = &Any{}
78+
79+
func (a *Any) MarshalJSON() ([]byte, error) {
80+
if a == nil {
81+
return []byte("null"), nil
82+
}
83+
return json.Marshal(a.Value)
84+
}
85+
86+
func (a *Any) UnmarshalJSON(data []byte) error {
87+
if data == nil || string(data) == "null" {
88+
a.Value = nil
89+
return nil
90+
}
91+
return json.Unmarshal(data, &a.Value)
92+
}
93+
94+
func (in *JSONPatch) DeepCopy() *JSONPatch {
95+
if in == nil {
96+
return nil
97+
}
98+
out := &JSONPatch{}
99+
in.DeepCopyInto(out)
100+
return out
101+
}
102+
103+
func (in *JSONPatch) DeepCopyInto(out *JSONPatch) {
104+
if out == nil {
105+
return
106+
}
107+
out.Operation = in.Operation
108+
out.Path = in.Path
109+
if in.Value != nil {
110+
valueCopy := *in.Value
111+
out.Value = &valueCopy
112+
} else {
113+
out.Value = nil
114+
}
115+
if in.From != nil {
116+
fromCopy := *in.From
117+
out.From = &fromCopy
118+
} else {
119+
out.From = nil
120+
}
121+
}
122+
123+
func (in *JSONPatches) DeepCopy() *JSONPatches {
124+
if in == nil {
125+
return nil
126+
}
127+
out := &JSONPatches{}
128+
for _, item := range *in {
129+
outItem := item.DeepCopy()
130+
*out = append(*out, *outItem)
131+
}
132+
return out
133+
}
134+
135+
func (in *JSONPatches) DeepCopyInto(out *JSONPatches) {
136+
if out == nil {
137+
return
138+
}
139+
*out = make(JSONPatches, len(*in))
140+
for i, item := range *in {
141+
outItem := item.DeepCopy()
142+
(*out)[i] = *outItem
143+
}
144+
}
145+
146+
func (in *Any) DeepCopy() *Any {
147+
if in == nil {
148+
return nil
149+
}
150+
out := &Any{}
151+
in.DeepCopyInto(out)
152+
return out
153+
}
154+
155+
func (in *Any) DeepCopyInto(out *Any) {
156+
if out == nil {
157+
return
158+
}
159+
if in.Value == nil {
160+
out.Value = nil
161+
return
162+
}
163+
// Use json.Marshal and json.Unmarshal to deep copy the value.
164+
data, err := json.Marshal(in.Value)
165+
if err != nil {
166+
panic("failed to marshal Any value: " + err.Error())
167+
}
168+
if err := json.Unmarshal(data, &out.Value); err != nil {
169+
panic("failed to unmarshal Any value: " + err.Error())
170+
}
171+
}
172+
173+
// Ptr is a convenience function to create a pointer to the given value.
174+
func Ptr[T any](val T) *T {
175+
return &val
176+
}

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- [Controller Utility Functions](libs/controller.md)
1010
- [Custom Resource Definitions](libs/crds.md)
1111
- [Error Handling](libs/errors.md)
12+
- [JSON Patch](libs/jsonpatch.md)
1213
- [Logging](libs/logging.md)
1314
- [Key-Value Pairs](libs/pairs.md)
1415
- [Readiness Checks](libs/readiness.md)

docs/libs/jsonpatch.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# JSON Patch
2+
3+
The `api/jsonpatch` package contains a `JSONPatches` type that represents a [JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902).
4+
The type is ready to be used in a kubernetes resource type.
5+
6+
The corresponding `pkg/jsonpatch` package contains helper functions to apply JSON patches specified via the aforementioned API type to a given JSON document or arbitrary go type.
7+
8+
## Embedding the API Type
9+
10+
```golang
11+
import jpapi "github.com/openmcp-project/controller-utils/api/jsonpatch"
12+
13+
type MyTypeSpec struct {
14+
Patches jpapi.JSONPatches `json:"patches"`
15+
}
16+
```
17+
18+
## Applying the Patches
19+
20+
### To a JSON Document
21+
22+
```golang
23+
import "github.com/openmcp-project/controller-utils/pkg/jsonpatch"
24+
25+
// mytype.Spec is of type MyTypeSpec as defined in the above example
26+
patch := jsonpatch.New(mytype.Spec.Patches)
27+
// doc and modified are of type []byte
28+
modified, err := patch.Apply(doc)
29+
```
30+
31+
### To an Arbitrary Type
32+
33+
The library supports applying JSON patches to arbitrary types. Internally, the object is marshalled to JSON, then the patch is applied, and then the object is unmarshalled into its original type again. The usual limitations of JSON (un)marshalling (no cyclic structures, etc.) apply.
34+
35+
```golang
36+
import "github.com/openmcp-project/controller-utils/pkg/jsonpatch"
37+
38+
// mytype.Spec is of type MyTypeSpec as defined in the above example
39+
patch := jsonpatch.NewTyped[MyPatchedType](mytype.Spec.Patches)
40+
// obj and modified are of type MyPatchedType
41+
modified, err := patch.Apply(doc)
42+
```
43+
44+
### Options
45+
46+
The `Apply` method optionally takes some options which can be constructed from functions contained in the package:
47+
```golang
48+
modified, err := patch.Apply(doc, jsonpatch.Indent(" "))
49+
```
50+
51+
The available options are:
52+
- `SupportNegativeIndices`
53+
- `AccumulatedCopySizeLimit`
54+
- `AllowMissingPathOnRemove`
55+
- `EnsurePathExistsOnAdd`
56+
- `EscapeHTML`
57+
- `Indent`
58+
59+
The options are simply passed into the [library which is used internally](https://github.com/evanphx/json-patch).

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ module github.com/openmcp-project/controller-utils
22

33
go 1.24.2
44

5+
replace github.com/openmcp-project/controller-utils/api => ./api
6+
57
require (
8+
github.com/evanphx/json-patch/v5 v5.9.11
69
github.com/go-logr/logr v1.4.3
710
github.com/go-logr/zapr v1.3.0
811
github.com/onsi/ginkgo v1.16.5
912
github.com/onsi/ginkgo/v2 v2.23.4
1013
github.com/onsi/gomega v1.37.0
14+
github.com/openmcp-project/controller-utils/api v0.0.0-00010101000000-000000000000
1115
github.com/spf13/pflag v1.0.6
1216
github.com/stretchr/testify v1.10.0
1317
go.uber.org/zap v1.27.0
@@ -29,7 +33,6 @@ require (
2933
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
3034
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
3135
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
32-
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
3336
github.com/fsnotify/fsnotify v1.8.0 // indirect
3437
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
3538
github.com/go-openapi/jsonpointer v0.21.0 // indirect

0 commit comments

Comments
 (0)