Skip to content

Commit 5ffa7ae

Browse files
authored
introduce pkg/fieldpath for processing field paths (#248)
In attempting to implement the nested field path support for different source and target field Go types, I realized I needed a support package that made working with field paths easier, including taking a field path and finding a ShapeRef within an Output shape that matched the field path. This patch introduces a new `pkg/fieldpath` package with a simple `Path` struct containing a variety of utility methods, as shown in this example Go code snippet: ```go import ( "fmt" awssdkmodel "github.com/aws/aws-sdk-go/private/model/api" "github.com/aws-controllers-k8s/code-generator/pkg/fieldpath" "github.com/aws-controllers-k8s/code-generator/pkg/sdk" ) ... p := fieldpath.FromString("Crawler.Schedule.ScheduleExpression") // Will print "true" fmt.Println(p.HasPrefix("Crawler.Schedule")) // Will print "false" fmt.Println(p.HasPrefix("crawler.Schedule")) // Will print "true" fmt.Println(p.HasPrefixFold("crawler.Schedule")) // Will print "ScheduleExpression" fmt.Println(p.Back()) // Will print "Crawler" fmt.Println(p.Front()) // Find the shape within the GetCrawlerResponse shape that contains the // field referred to by the path... helper := sdk.NewHelper("/path/to/apis", genCfg) api := helper.API("glue") crawlerOutputShape := api.Shapes["GetCrawlerResponse"] schedExpressionShape := p.ShapeRef(crawlerOutputShape) // Will print "ScheduleExpression" fmt.Println(schedExpressionShape.ShapeName) ``` Note that we support list and map type field paths using a single-dot notation since we are only looking at field types and not **values** within the list or map. Signed-off-by: Jay Pipes <[email protected]> Related: aws-controllers-k8s/community#1078 By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 966e9a9 commit 5ffa7ae

File tree

2 files changed

+434
-0
lines changed

2 files changed

+434
-0
lines changed

pkg/fieldpath/path.go

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
// not use this file except in compliance with the License. A copy of the
5+
// License is located at
6+
//
7+
// http://aws.amazon.com/apache2.0/
8+
//
9+
// or in the "license" file accompanying this file. This file is distributed
10+
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
// express or implied. See the License for the specific language governing
12+
// permissions and limitations under the License.
13+
14+
package fieldpath
15+
16+
import (
17+
"encoding/json"
18+
"strings"
19+
20+
awssdkmodel "github.com/aws/aws-sdk-go/private/model/api"
21+
)
22+
23+
// Path provides a JSONPath-like struct and field-member "route" to a
24+
// particular field within a resource. Path implements json.Marshaler
25+
// interface.
26+
type Path struct {
27+
parts []string
28+
}
29+
30+
// String returns the dotted-notation representation of the Path
31+
func (p *Path) String() string {
32+
return strings.Join(p.parts, ".")
33+
}
34+
35+
// MarshalJSON returns the JSON encoding of a Path object.
36+
func (p *Path) MarshalJSON() ([]byte, error) {
37+
// Since json.Marshal doesn't encode unexported struct fields we have to
38+
// copy the Path instance into a new struct object with exported fields.
39+
// See https://github.com/aws-controllers-k8s/community/issues/772
40+
return json.Marshal(
41+
struct {
42+
Parts []string
43+
}{
44+
p.parts,
45+
},
46+
)
47+
}
48+
49+
// Pop removes the last part from the Path and returns it.
50+
func (p *Path) Pop() (part string) {
51+
if len(p.parts) > 0 {
52+
part = p.parts[len(p.parts)-1]
53+
p.parts = p.parts[:len(p.parts)-1]
54+
}
55+
return part
56+
}
57+
58+
// Front returns the first part of the Path or empty string if the Path has no
59+
// parts.
60+
func (p *Path) Front() string {
61+
if len(p.parts) == 0 {
62+
return ""
63+
}
64+
return p.parts[0]
65+
}
66+
67+
// PopFront removes the first part of the Path and returns it.
68+
func (p *Path) PopFront() (part string) {
69+
if len(p.parts) > 0 {
70+
part = p.parts[0]
71+
p.parts = p.parts[1:]
72+
}
73+
return part
74+
}
75+
76+
// Back returns the last part of the Path or empty string if the Path has no
77+
// parts.
78+
func (p *Path) Back() string {
79+
if len(p.parts) == 0 {
80+
return ""
81+
}
82+
return p.parts[len(p.parts)-1]
83+
}
84+
85+
// PushBack adds a new part to the end of the Path.
86+
func (p *Path) PushBack(part string) {
87+
p.parts = append(p.parts, part)
88+
}
89+
90+
// Copy returns a new Path that is a copy of this Path
91+
func (p *Path) Copy() *Path {
92+
return &Path{p.parts}
93+
}
94+
95+
// Empty returns true if there are no parts to the Path
96+
func (p *Path) Empty() bool {
97+
return len(p.parts) == 0
98+
}
99+
100+
// ShapeRef returns an aws-sdk-go ShapeRef within the supplied ShapeRef that
101+
// matches the Path. Returns nil if no matching ShapeRef could be found.
102+
//
103+
// Assume a ShapeRef that looks like this:
104+
//
105+
// authShapeRef := &awssdkmodel.ShapeRef{
106+
// ShapeName: "Author",
107+
// Shape: &awssdkmodel.Shape{
108+
// Type: "structure",
109+
// MemberRefs: map[string]*awssdkmodel.ShapeRef{
110+
// "Name": &awssdkmodel.ShapeRef{
111+
// ShapeName: "Name",
112+
// Shape: &awssdkmodel.Shape{
113+
// Type: "string",
114+
// },
115+
// },
116+
// "Address": &awssdkmodel.ShapeRef{
117+
// ShapeName: "Address",
118+
// Shape: &awssdkmodel.Shape{
119+
// Type: "structure",
120+
// MemberRefs: map[string]*awssdkmodel.ShapeRef{
121+
// "State": &awssdkmodel.ShapeRef{
122+
// ShapeName: "StateCode",
123+
// Shape: &awssdkmodel.Shape{
124+
// Type: "string",
125+
// },
126+
// },
127+
// "Country": &awssdkmodel.ShapeRef{
128+
// ShapeName: "CountryCode",
129+
// Shape: &awssdkmodel.Shape{
130+
// Type: "string",
131+
// },
132+
// },
133+
// },
134+
// },
135+
// },
136+
// },
137+
// },
138+
// }
139+
//
140+
// If I have the following Path:
141+
//
142+
// p := fieldpath.FromString("Author.Address.Country")
143+
//
144+
// calling p.ShapeRef(authShapeRef) would return the following:
145+
//
146+
// &awssdkmodel.ShapeRef{
147+
// ShapeName: "CountryCode",
148+
// Shape: &awssdkmodel.Shape{
149+
// Type: "string",
150+
// },
151+
// },
152+
func (p *Path) ShapeRef(
153+
subject *awssdkmodel.ShapeRef,
154+
) *awssdkmodel.ShapeRef {
155+
if subject == nil || p == nil || len(p.parts) == 0 {
156+
return nil
157+
}
158+
159+
// We first check that the first part in the path matches the supplied
160+
// subject shape's name.
161+
var compare *awssdkmodel.ShapeRef = subject
162+
cp := p.Copy()
163+
cur := cp.PopFront()
164+
if compare.ShapeName != cur {
165+
return nil
166+
}
167+
// And then we walk through the path, searching through the supplied
168+
// ShapeRef for a member ShapeRef matching each path element.
169+
for !cp.Empty() {
170+
cur = cp.PopFront()
171+
if compare = memberShapeRef(compare, cur); compare == nil {
172+
return nil
173+
}
174+
}
175+
return compare
176+
}
177+
178+
// memberShapeRef returns the named member ShapeRef of the supplied
179+
// ShapeRef
180+
func memberShapeRef(
181+
shapeRef *awssdkmodel.ShapeRef,
182+
memberName string,
183+
) *awssdkmodel.ShapeRef {
184+
if shapeRef.ShapeName == memberName {
185+
return shapeRef
186+
}
187+
switch shapeRef.Shape.Type {
188+
case "structure":
189+
return shapeRef.Shape.MemberRefs[memberName]
190+
case "list":
191+
return memberShapeRef(&shapeRef.Shape.MemberRef, memberName)
192+
case "map":
193+
return memberShapeRef(&shapeRef.Shape.ValueRef, memberName)
194+
}
195+
return nil
196+
}
197+
198+
// HasPrefix returns true if the supplied string, delimited on ".", matches
199+
// p.parts up to the length of the supplied string.
200+
// e.g. if the Path p represents "A.B":
201+
// subject "A" -> true
202+
// subject "A.B" -> true
203+
// subject "A.B.C" -> false
204+
// subject "B" -> false
205+
// subject "A.C" -> false
206+
func (p *Path) HasPrefix(subject string) bool {
207+
subjectSplit := strings.Split(subject, ".")
208+
209+
if len(subjectSplit) > len(p.parts) {
210+
return false
211+
}
212+
213+
for i, s := range subjectSplit {
214+
if p.parts[i] != s {
215+
return false
216+
}
217+
}
218+
219+
return true
220+
}
221+
222+
// HasPrefixFold is the same as HasPrefix but uses case-insensitive comparisons
223+
func (p *Path) HasPrefixFold(subject string) bool {
224+
subjectSplit := strings.Split(subject, ".")
225+
226+
if len(subjectSplit) > len(p.parts) {
227+
return false
228+
}
229+
230+
for i, s := range subjectSplit {
231+
if !strings.EqualFold(p.parts[i], s) {
232+
return false
233+
}
234+
}
235+
236+
return true
237+
}
238+
239+
// FromString returns a new Path from a dotted-notation string, e.g.
240+
// "Author.Name".
241+
func FromString(dotted string) *Path {
242+
return &Path{strings.Split(dotted, ".")}
243+
}

0 commit comments

Comments
 (0)