aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--human.go96
-rw-r--r--human_test.go99
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)
+ }
+}