git.s-ol.nu json-hopper / 751f82f
initial commit s-ol 1 year, 2 months ago
4 changed file(s) with 262 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 data
0 module s-ol.nu/json-hopper
1
2 go 1.16
3
4 require (
5 github.com/google/uuid v1.3.0 // indirect
6 github.com/santhosh-tekuri/jsonschema/v5 v5.0.0
7 )
0 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
1 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
2 github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 h1:TToq11gyfNlrMFZiYujSekIsPd9AmsA2Bj/iv+s4JHE=
3 github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0=
0 package main
1
2 import (
3 "encoding/json"
4 "github.com/google/uuid"
5 jsonschema "github.com/santhosh-tekuri/jsonschema/v5"
6 "log"
7 "net/http"
8 "os"
9 "path/filepath"
10 "strconv"
11 "strings"
12 "time"
13 )
14
15 type HTTPError struct {
16 code int
17 root error
18 }
19
20 func (e *HTTPError) Error() string {
21 msg := http.StatusText(e.code)
22 if e.root != nil {
23 msg += ": " + e.root.Error()
24 }
25 return msg
26 }
27
28 type SiteHandler struct {
29 name string
30 path string
31 schema *jsonschema.Schema
32 }
33
34 func NewSiteHandler(site_name string, site_path string) (*SiteHandler, error) {
35 schema, err := jsonschema.Compile(filepath.Join(site_path, ".schema.json"))
36 if err != nil {
37 return nil, err
38 }
39 return &SiteHandler{site_name, site_path, schema}, nil
40 }
41
42 func NewMeta() (map[string]interface{}, error) {
43 id, err := uuid.NewUUID()
44 if err != nil {
45 return nil, err
46 }
47
48 key, err := uuid.NewRandom()
49 if err != nil {
50 return nil, err
51 }
52
53 now := time.Now().UTC().String()
54
55 data := make(map[string]interface{})
56 data["id"] = id.String()
57 data["key"] = key.String()
58 data["submitted"] = now
59
60 return data, nil
61 }
62
63 func processValue(typ string, value string) interface{} {
64 switch typ {
65 case "integer":
66 val, err := strconv.Atoi(value)
67 if err != nil {
68 return value
69 }
70 return val
71 case "number":
72 val, err := strconv.ParseFloat(value, 64)
73 if err != nil {
74 return value
75 }
76 return val
77 case "boolean":
78 return value == "true"
79 default:
80 return value
81 }
82 }
83
84 func (h *SiteHandler) collectData(r *http.Request) (map[string]interface{}, error) {
85 err := r.ParseMultipartForm(0)
86 if err != nil {
87 return nil, &HTTPError{http.StatusBadRequest, err}
88 }
89
90 data := make(map[string]interface{})
91 for key, values := range r.PostForm {
92 if key == "_key" {
93 continue
94 }
95
96 key_schema, ok := h.schema.Properties[key]
97
98 val_type := ""
99 if ok && key_schema.Types[0] == "array" {
100 if key_schema.Items2020 != nil {
101 val_type = key_schema.Items2020.Types[0]
102 }
103 } else if ok {
104 val_type = key_schema.Types[0]
105 }
106
107 if ok && key_schema.Types[0] != "array" && len(values) == 1 {
108 data[key] = processValue(val_type, values[0])
109 } else {
110 tmp := make([]interface{}, len(values))
111 for i := range values {
112 tmp[i] = processValue(val_type, values[i])
113 }
114
115 data[key] = tmp
116 }
117 }
118
119 return data, nil
120 }
121
122 func (h *SiteHandler) handlePUTPOST(w http.ResponseWriter, r *http.Request, meta map[string]interface{}) error {
123 data, err := h.collectData(r)
124 if err != nil {
125 return err
126 }
127
128 if err := h.schema.Validate(data); err != nil {
129 return &HTTPError{http.StatusNotAcceptable, nil}
130 }
131
132 if meta == nil {
133 meta, err = NewMeta()
134 if err != nil {
135 return err
136 }
137 } else {
138 meta["updated"] = time.Now().UTC().String()
139 }
140 data["$meta"] = meta
141
142 bytes, err := json.Marshal(data)
143 if err != nil {
144 return err
145 }
146
147 if err := os.WriteFile(
148 filepath.Join(h.path, meta["id"].(string)+".json"),
149 bytes,
150 0644,
151 ); err != nil {
152 return err
153 }
154
155 log.Printf("%s %s", r.Method, meta["id"])
156 w.Header().Set("Content-Type", "application/json")
157 w.Write(bytes)
158 return nil
159 }
160
161 func (h *SiteHandler) handle(w http.ResponseWriter, r *http.Request) error {
162 parts := strings.Split(r.URL.Path, "/")
163
164 if len(parts) != 3 || parts[1] != h.name {
165 return &HTTPError{http.StatusInternalServerError, nil}
166 }
167
168 id := parts[2]
169
170 if id == "" {
171 if r.Method != "POST" {
172 return &HTTPError{http.StatusMethodNotAllowed, nil}
173 }
174
175 return h.handlePUTPOST(w, r, nil)
176 }
177
178 var data map[string]interface{}
179 bytes, err := os.ReadFile(filepath.Join(h.path, id+".json"))
180 if err != nil {
181 return &HTTPError{http.StatusNotFound, nil}
182 }
183
184 if err := json.Unmarshal(bytes, &data); err != nil {
185 return err
186 }
187
188 meta := data["$meta"].(map[string]interface{})
189 key := r.FormValue("_key")
190 if meta["key"] != key {
191 return &HTTPError{http.StatusUnauthorized, nil}
192 }
193
194 if r.Method == "PUT" {
195 return h.handlePUTPOST(w, r, meta)
196 } else if r.Method == "GET" {
197 w.Header().Set("Content-Type", "application/json")
198 w.Write(bytes)
199 return nil
200 }
201
202 return &HTTPError{http.StatusMethodNotAllowed, nil}
203 }
204
205 func (h *SiteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
206 err := h.handle(w, r)
207
208 if err != nil {
209 var code int
210
211 switch e := err.(type) {
212 case *HTTPError:
213 code = e.code
214 if e.root != nil {
215 log.Printf("Error: %#v", err)
216 }
217 default:
218 code = http.StatusInternalServerError
219 log.Printf("Error: %#v", err)
220 }
221
222 http.Error(w, http.StatusText(code), code)
223 }
224 }
225
226 func main() {
227 sites, err := filepath.Glob(os.Args[1] + "/*")
228 if err != nil {
229 log.Fatalf("Error locating sites: %#v", err)
230 }
231
232 for _, site_path := range sites {
233 site_name := filepath.Base(site_path)
234 log.Printf("loading site %s", site_name)
235 handler, err := NewSiteHandler(site_name, site_path)
236 if err != nil {
237 log.Fatalf("NewSiteHandler %s: ", site_path, err)
238 }
239
240 http.Handle("/"+site_name+"/", handler)
241 }
242
243 log.Printf("ready, listening on :3000")
244 err = http.ListenAndServe(":3000", nil) // setting listening port
245 if err != nil {
246 log.Fatal("ListenAndServe: ", err)
247 }
248 }