-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy pathobject.go
199 lines (162 loc) · 5.32 KB
/
object.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
package jsh
import (
"encoding/json"
"fmt"
"net/http"
"github.com/asaskevich/govalidator"
)
// Object represents the default JSON spec for objects
type Object struct {
Type string `json:"type" valid:"required"`
ID string `json:"id"`
Attributes json.RawMessage `json:"attributes,omitempty"`
Links map[string]*Link `json:"links,omitempty"`
Relationships map[string]*Relationship `json:"relationships,omitempty"`
Meta map[string]interface{} `json:"meta,omitempty"`
// Status is the HTTP Status Code that should be associated with the object
// when it is sent.
Status int `json:"-"`
}
// NewObject prepares a new JSON Object for an API response. Whatever is provided
// as attributes will be marshalled to JSON.
func NewObject(id string, resourceType string, attributes interface{}) (*Object, *Error) {
object := &Object{
ID: id,
Type: resourceType,
Links: map[string]*Link{},
Relationships: map[string]*Relationship{},
}
rawJSON, err := json.MarshalIndent(attributes, "", " ")
if err != nil {
return nil, ISE(fmt.Sprintf("Error marshaling attrs while creating a new JSON Object: %s", err))
}
object.Attributes = rawJSON
return object, nil
}
/*
Unmarshal puts an Object's Attributes into a more useful target resourceType defined
by the user. A correct object resourceType specified must also be provided otherwise
an error is returned to prevent hard to track down situations.
Optionally, used https://github.com/go-validator/validator for request input validation.
Simply define your struct with valid input tags:
struct {
Username string `json:"username" valid:"required,alphanum"`
}
As the final action, the Unmarshal function will run govalidator on the unmarshal
result. If the validator fails, a Sendable error response of HTTP Status 422 will
be returned containing each validation error with a populated Error.Source.Pointer
specifying each struct attribute that failed. In this case, all you need to do is:
errors := obj.Unmarshal("mytype", &myType)
if errors != nil {
// log errors via error.ISE
jsh.Send(w, r, errors)
}
*/
func (o *Object) Unmarshal(resourceType string, target interface{}) ErrorList {
if resourceType != o.Type {
return []*Error{ISE(fmt.Sprintf(
"Expected type %s, when converting actual type: %s",
resourceType,
o.Type,
))}
}
jsonErr := json.Unmarshal(o.Attributes, target)
if jsonErr != nil {
return []*Error{ISE(fmt.Sprintf(
"For type '%s' unable to marshal: %s\nError:%s",
resourceType,
string(o.Attributes),
jsonErr.Error(),
))}
}
return validateInput(target)
}
/*
Marshal allows you to load a modified payload back into an object to preserve
all of the data it has.
*/
func (o *Object) Marshal(attributes interface{}) *Error {
raw, err := json.MarshalIndent(attributes, "", " ")
if err != nil {
return ISE(fmt.Sprintf("Error marshaling attrs while creating a new JSON Object: %s", err))
}
o.Attributes = raw
return nil
}
/*
Validate ensures that an object is JSON API compatible. Has a side effect of also
setting the Object's Status attribute to be used as the Response HTTP Code if one
has not already been set.
*/
func (o *Object) Validate(r *http.Request, response bool) *Error {
if o.ID == "" {
// don't error if the client is attempting to performing a POST request, in
// which case, ID shouldn't actually be set
if !response && r.Method != "POST" {
return SpecificationError("ID must be set for Object response")
}
}
if o.Type == "" {
return SpecificationError("Type must be set for Object response")
}
switch r.Method {
case "POST":
acceptable := map[int]bool{201: true, 202: true, 204: true}
if o.Status != 0 {
if _, validCode := acceptable[o.Status]; !validCode {
return SpecificationError("POST Status must be one of 201, 202, or 204.")
}
break
}
o.Status = http.StatusCreated
break
case "PATCH":
acceptable := map[int]bool{200: true, 202: true, 204: true}
if o.Status != 0 {
if _, validCode := acceptable[o.Status]; !validCode {
return SpecificationError("PATCH Status must be one of 200, 202, or 204.")
}
break
}
o.Status = http.StatusOK
break
case "GET":
o.Status = http.StatusOK
break
// If we hit this it means someone is attempting to use an unsupported HTTP
// method. Return a 406 error instead
default:
return SpecificationError(fmt.Sprintf(
"The JSON Specification does not accept '%s' requests.",
r.Method,
))
}
return nil
}
// String prints a formatted string representation of the object
func (o *Object) String() string {
raw, err := json.MarshalIndent(o, "", " ")
if err != nil {
return err.Error()
}
return string(raw)
}
// validateInput runs go-validator on each attribute on the struct and returns all
// errors that it picks up
func validateInput(target interface{}) ErrorList {
_, validationError := govalidator.ValidateStruct(target)
if validationError != nil {
errorList, isType := validationError.(govalidator.Errors)
if isType {
errors := ErrorList{}
for _, singleErr := range errorList.Errors() {
// parse out validation error
goValidErr, _ := singleErr.(govalidator.Error)
inputErr := InputError(goValidErr.Err.Error(), goValidErr.Name)
errors = append(errors, inputErr)
}
return errors
}
}
return nil
}