Skip to content

Commit 89b0bef

Browse files
authored
Update section on unions and enumerated subtypes
1 parent 393f38e commit 89b0bef

File tree

1 file changed

+136
-71
lines changed

1 file changed

+136
-71
lines changed

generator/README.md

Lines changed: 136 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,31 @@ type Account struct {
5656
}
5757
```
5858

59+
#### Inheritance
60+
61+
Stone supports [struct inheritance](https://github.com/dropbox/stone/blob/master/doc/lang_ref.rst#inheritance). In Go, we support this via [embedding](https://golang.org/doc/effective_go.html#embedding)
62+
63+
```
64+
struct BasicAccount extends Account
65+
"Basic information about any account."
66+
67+
is_teammate Boolean
68+
"Whether this user is a teammate of the current user. If this account
69+
is the current user's account, then this will be :val:`true`."
70+
```
71+
72+
```go
73+
// Basic information about any account.
74+
type BasicAccount struct {
75+
Account
76+
// Whether this user is a teammate of the current user. If this account is
77+
// the current user's account, then this will be `True`.
78+
IsTeammate bool `json:"is_teammate"`
79+
```
80+
5981
### Unions
6082
61-
Stone https://github.com/dropbox/stone/blob/master/doc/lang_ref.rst#union[unions] are bit more complex as Go doesn't have native support for union types (tagged or otherwise). We declare a union as a Go struct with all the possible fields as pointer types, and then use the tag value to populate the correct field during deserialization. This necessitates the use of an intermedia wrapper struct for the deserialization to work correctly, see below for a concrete example.
83+
Stone https://github.com/dropbox/stone/blob/master/doc/lang_ref.rst#union[unions] are bit more complex as Go doesn't have native support for union types (tagged or otherwise). We declare a union as a Go struct with all the possible fields as pointer types, and then use the tag value to populate the correct field during deserialization. This necessitates the use of an intermediate wrapper struct for the deserialization to work correctly, see below for a concrete example.
6284
6385
```
6486
union SpaceAllocation
@@ -72,101 +94,144 @@ union SpaceAllocation
7294
7395
```go
7496
// Space is allocated differently based on the type of account.
75-
type SpaceAllocation struct { // <1>
76-
Tag string `json:".tag"` // <2>
77-
// The user's space allocation applies only to their individual account.
78-
Individual *IndividualSpaceAllocation `json:"individual,omitempty"` // <3>
79-
// The user shares space with other members of their team.
80-
Team *TeamSpaceAllocation `json:"team,omitempty"`
97+
type SpaceAllocation struct {
98+
dropbox.Tagged
99+
// The user's space allocation applies only to their individual account.
100+
Individual *IndividualSpaceAllocation `json:"individual,omitempty"`
101+
// The user shares space with other members of their team.
102+
Team *TeamSpaceAllocation `json:"team,omitempty"`
81103
}
82104

83-
func (u *SpaceAllocation) UnmarshalJSON(body []byte) error { // <4>
84-
type wrap struct { // <5>
85-
Tag string `json:".tag"`
86-
// The user's space allocation applies only to their individual account.
87-
Individual json.RawMessage `json:"individual"` // <6>
88-
// The user shares space with other members of their team.
89-
Team json.RawMessage `json:"team"`
90-
}
91-
var w wrap
92-
if err := json.Unmarshal(body, &w); err != nil { // <7>
93-
return err
94-
}
95-
u.Tag = w.Tag
96-
switch w.Tag {
97-
case "individual":
98-
{
99-
if err := json.Unmarshal(body, &u.Individual); err != nil { // <8>
100-
return err
101-
}
102-
}
103-
case "team":
104-
{
105-
if err := json.Unmarshal(body, &u.Team); err != nil {
106-
return err
107-
}
108-
}
109-
}
110-
return nil
105+
// Valid tag values for `SpaceAllocation`
106+
const (
107+
SpaceAllocation_Individual = "individual"
108+
SpaceAllocation_Team = "team"
109+
SpaceAllocation_Other = "other"
110+
)
111+
112+
func (u *SpaceAllocation) UnmarshalJSON(body []byte) error {
113+
type wrap struct {
114+
dropbox.Tagged
115+
// The user's space allocation applies only to their individual account.
116+
Individual json.RawMessage `json:"individual,omitempty"`
117+
// The user shares space with other members of their team.
118+
Team json.RawMessage `json:"team,omitempty"`
119+
}
120+
var w wrap
121+
if err := json.Unmarshal(body, &w); err != nil {
122+
return err
123+
}
124+
u.Tag = w.Tag
125+
switch u.Tag {
126+
case "individual":
127+
if err := json.Unmarshal(body, &u.Individual); err != nil {
128+
return err
129+
}
130+
131+
case "team":
132+
if err := json.Unmarshal(body, &u.Team); err != nil {
133+
return err
134+
}
135+
136+
}
137+
return nil
111138
}
112139
```
113-
<1> A babel union is represented as Go struct
114-
<2> The tag value is used to determine which field is actually set
115-
<3> Possible values are represented as pointer types. Note the `omitempty` in the JSON tag; this is so values not set are automatically elided during serialization.
116-
<4> We generate a custom `UnmarshalJSON` method for union types
117-
<5> An intermedia wrapper struct is used to help with deserialization
118-
<6> Note that members of the wrapper struct are of type `RawMessage` so we can capture the body without deserializing it right away
119-
<7> When we deserialize response into the wrapper struct, it should get the tag value and the raw JSON as part of the members.
120-
<8> We then use the tag value to deserialize the `RawMessage` into the appropriate member of the union type
121140
122141
### Struct with Enumerated Subtypes
123142
124143
Per the https://github.com/dropbox/stone/blob/master/doc/lang_ref.rst#struct-polymorphism[spec], structs with enumerated subtypes are a mechanism of inheritance:
125144
126-
> If a struct enumerates its subtypes, an instance of any subtype will satisfy the type constraint.
145+
> If a struct enumerates its subtypes, an instance of any subtype will satisfy the type constraint. This is useful when wanting to discriminate amongst types that are part of the same hierarchy while simultaneously being able to avoid discriminating when accessing common fields.
127146
128-
So to represent structs with enumerated subtypes in Go, we use a strategy similar to unions. The _base_ struct (the one that defines the subtypes) is represented like we represent a union above. The _subtype_ is represented as a struct that essentially duplicates all fields of the base type and includes fields specific to that subtype. Here's an example:
147+
To represent structs with enumerated subtypes in Go, we use a combination of Go interface types and unions as implemented above. Considering the following:
129148
130149
```
131150
struct Metadata
132-
"Metadata for a file or folder."
133-
134151
union
135152
file FileMetadata
136153
folder FolderMetadata
137154
deleted DeletedMetadata # Used by list_folder* and search
138155

139-
name String #<1>
140-
"The last component of the path (including extension).
141-
This never contains a slash."
142-
143-
...
156+
name String
157+
path_lower String?
158+
path_display String?
159+
parent_shared_folder_id common.SharedFolderId?
160+
144161
struct FileMetadata extends Metadata
145-
id Id? #<2>
146-
...
162+
id Id
163+
client_modified common.DropboxTimestamp
164+
...
147165
```
148-
<1> Field common to all subtypes
149-
<2> Field specific to `FileMetadata`
150166
167+
In this case, `FileMetadata`, `FolderMetadata` etc are subtypes of `Metadata`. Specifically, any subtype can be used where a parent type is expected. Thus, if `list_folder` returns a list of `Metadata`s, we should be able to parse and "upcast" to one of the enumerated subtypes.
168+
169+
First, we define structs to represent the base and enumerated types as we did for inherited structs above:
151170
152171
```go
153-
// Metadata for a file or folder.
154-
type Metadata struct { // <1>
155-
Tag string `json:".tag"`
156-
File *FileMetadata `json:"file,omitempty"`
157-
Folder *FolderMetadata `json:"folder,omitempty"`
158-
Deleted *DeletedMetadata `json:"deleted,omitempty"`
172+
type Metadata struct {
173+
Name string `json:"name"`
174+
PathLower string `json:"path_lower,omitempty"`
175+
PathDisplay string `json:"path_display,omitempty"`
176+
ParentSharedFolderId string `json:"parent_shared_folder_id,omitempty"`
159177
}
160-
...
161178

162179
type FileMetadata struct {
163-
// The last component of the path (including extension). This never contains a
164-
// slash.
165-
Name string `json:"name"` // <2>
166-
...
167-
Id string `json:"id,omitempty"` // <3>
180+
Metadata
181+
Id string `json:"id"`
182+
ClientModified time.Time `json:"client_modified"`
183+
...
184+
}
185+
```
186+
187+
Next, we define an interface type with a dummy method and ensure that both the base and the subtypes implement the interface:
188+
189+
```go
190+
type IsMetadata interface {
191+
IsMetadata()
192+
}
193+
194+
func (u *Metadata) IsMetadata() {} // Subtypes get this for free due to embedding
195+
```
196+
197+
At this point, types or methods that accept/return a struct with enumerated subtypes can use the interface type instead. For instance:
198+
199+
```go
200+
func GetMetadata(arg *GetMetadataArg) (res IsMetadata, err error) {...}
201+
202+
type ListFolderResult struct {
203+
// The files and (direct) subfolders in the folder.
204+
Entries []IsMetadata `json:"entries"`
205+
...
206+
}
207+
```
208+
209+
Finally, to actually deserialize a bag of bytes into the appropriate type or subtype, we use a trick similar to how we handle unions above.
210+
211+
```
212+
type metadataUnion struct {
213+
dropbox.Tagged
214+
File *FileMetadata `json:"file,omitempty"`
215+
Folder *FolderMetadata `json:"folder,omitempty"`
216+
Deleted *DeletedMetadata `json:"deleted,omitempty"`
217+
}
218+
219+
func (u *metadataUnion) UnmarshalJSON(body []byte) error {...}
220+
221+
func (dbx *apiImpl) GetMetadata(arg *GetMetadataArg) (res IsMetadata, err error) {
222+
...
223+
var tmp metadataUnion
224+
err = json.Unmarshal(body, &tmp)
225+
if err != nil {
226+
return
227+
}
228+
switch tmp.Tag {
229+
case "file":
230+
res = tmp.File
231+
case "folder":
232+
res = tmp.Folder
233+
case "deleted":
234+
res = tmp.Deleted
235+
}
168236
}
169237
```
170-
<1> Subtype is represented like we represent unions as described above
171-
<2> Common fields are duplicated in subtypes
172-
<3> Subtype specific fields are included as usual in structs

0 commit comments

Comments
 (0)