diff options
| author | Anhgelus Morhtuuzh <william@herges.fr> | 2026-03-15 14:01:24 +0100 |
|---|---|---|
| committer | Anhgelus Morhtuuzh <william@herges.fr> | 2026-03-15 14:01:24 +0100 |
| commit | 1ee4d17b20480b2d34ef289deab57fef77729b58 (patch) | |
| tree | 4d1ccf0262c81548efbe3c86fb9cdb84693e2f6a | |
| parent | 16b8e2eee103def961ee6871d7e9ae2626b99475 (diff) | |
feat(human): json operations
| -rw-r--r-- | human.go | 96 | ||||
| -rw-r--r-- | human_test.go | 99 |
2 files changed, 195 insertions, 0 deletions
diff --git a/human.go b/human.go new file mode 100644 index 0000000..d78d31f --- /dev/null +++ b/human.go @@ -0,0 +1,96 @@ +package human + +import ( + "encoding/json" + "errors" + "net/url" + "strings" + "time" +) + +const Version = "0.1.1" + +var ( + stdPorts = map[string]string{ + "http": "80", "https": "443", "gemini": "1965", + } +) + +var ( + ErrInvalidURL = errors.New("invalid url") +) + +// URL represents a standard [url.URL] following normalization rules of human.json. +type URL url.URL + +func (u *URL) normalize() { + if strings.Contains(u.Host, ":") { + sp := strings.Split(u.Host, ":") + if v, ok := stdPorts[u.Scheme]; ok && v == sp[1] { + u.Host = sp[0] + } + } + u.Path = strings.TrimSuffix(u.Path, "/") +} + +func (u *URL) UnmarshalJSON(b []byte) error { + var s string + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + raw, err := url.Parse(s) + if err != nil { + return err + } + *u = URL(*raw) + u.normalize() + return nil +} + +func (u *URL) MarshalJSON() ([]byte, error) { + u.normalize() + return json.Marshal(u.String()) +} + +func (u *URL) String() string { + ur := url.URL(*u) + return ur.String() +} + +// Vouch a website to trust it. +type Vouch struct { + URL *URL `json:"url"` + VouchedAt time.Time `json:"-"` +} + +func (v *Vouch) UnmarshalJSON(b []byte) error { + type t Vouch + var r struct { + t + VouchedAt string `json:"vouched_at"` + } + err := json.Unmarshal(b, &r) + if err != nil { + return err + } + *v = Vouch(r.t) + v.VouchedAt, err = time.Parse(time.DateOnly, r.VouchedAt) + return err +} + +func (v *Vouch) MarshalJSON() ([]byte, error) { + type t Vouch + r := struct { + t + VouchedAt string `json:"vouched_at"` + }{t(*v), v.VouchedAt.Format(time.DateOnly)} + return json.Marshal(r) +} + +// Human represents the human.json file. +type Human struct { + Version string `json:"string"` + URL *URL `json:"url"` + Vouches []*Vouch `json:"vouches"` +} diff --git a/human_test.go b/human_test.go new file mode 100644 index 0000000..5109af4 --- /dev/null +++ b/human_test.go @@ -0,0 +1,99 @@ +package human_test + +import ( + "encoding/json" + "fmt" + "net/url" + "testing" + "time" + + "git.anhgelus.world/anhgelus/go-human.json" +) + +func TestURL_Json(t *testing.T) { + var r human.URL + err := json.Unmarshal([]byte(`"http://example.com"`), &r) + if err != nil { + t.Fatal(err) + } + if r.Scheme != "http" { + t.Errorf("invalid scheme: %s", r.Scheme) + } + if r.Host != "example.com" { + t.Errorf("invalid host: %s", r.Host) + } + if r.Path != "" { + t.Errorf("invalid path: %s", r.Path) + } + + err = json.Unmarshal([]byte(`"http://example.com:80/"`), &r) + if err != nil { + t.Fatal(err) + } + if r.Scheme != "http" { + t.Errorf("invalid scheme: %s", r.Scheme) + } + if r.Host != "example.com" { + t.Errorf("invalid host: %s", r.Host) + } + if r.Path != "" { + t.Errorf("invalid path: %s", r.Path) + } + + err = json.Unmarshal([]byte(`"https://foo.example.com:443/hello/world/"`), &r) + if err != nil { + t.Fatal(err) + } + if r.Scheme != "https" { + t.Errorf("invalid scheme: %s", r.Scheme) + } + if r.Host != "foo.example.com" { + t.Errorf("invalid host: %s", r.Host) + } + if r.Path != "/hello/world" { + t.Errorf("invalid path: %s", r.Path) + } + + err = json.Unmarshal([]byte(`"https://example.com:80/hello/world/?foo=bar"`), &r) + if err != nil { + t.Fatal(err) + } + if r.Scheme != "https" { + t.Errorf("invalid scheme: %s", r.Scheme) + } + if r.Host != "example.com:80" { + t.Errorf("invalid host: %s", r.Host) + } + if r.Path != "/hello/world" { + t.Errorf("invalid path: %s", r.Path) + } +} + +func TestVouch_Json(t *testing.T) { + var v human.Vouch + err := json.Unmarshal([]byte(`{"url": "https://bob.example.com","vouched_at": "2026-01-15"}`), &v) + if err != nil { + t.Fatal(err) + } + if v.URL.String() != "https://bob.example.com" { + t.Errorf("invalid url: %v", v.URL) + } + if v.VouchedAt.Format(time.DateOnly) != "2026-01-15" { + t.Errorf("invalid vouched_at: %v", v.VouchedAt) + } + + raw, _ := url.Parse(`https://bob.example.com:443/`) + now := time.Now() + cv := human.URL(*raw) + v = human.Vouch{ + URL: &cv, + VouchedAt: now, + } + b, err := json.Marshal(&v) + if err != nil { + t.Fatal(err) + } + if string(b) != fmt.Sprintf(`{"url":"https://bob.example.com","vouched_at":"%s"}`, now.Format(time.DateOnly)) { + t.Errorf("invalid marshal: %s", b) + } +} |
