Kaynağa Gözat

object: separated into its own package to make it easier to test

Benton Edmondson 2 yıl önce
ebeveyn
işleme
58f499d69a
2 değiştirilmiş dosya ile 278 ekleme ve 0 silme
  1. 150 0
      object/object.go
  2. 128 0
      object/object_test.go

+ 150 - 0
object/object.go

@@ -0,0 +1,150 @@
+package object
+
+import (
+	"errors"
+	"net/url"
+	"time"
+	"mimicry/mime"
+	"mimicry/render"
+	"fmt"
+)
+
+type Object map[string]any
+
+var (
+	ErrKeyNotPresent = errors.New("key is not present")
+	ErrKeyWrongType = errors.New("key is incorrect type")
+)
+
+/* Go doesn't allow generic methods */
+func getPrimitive[T any](o Object, key string) (T, error) {
+	var zero T
+	if value, ok := o[key]; !ok {
+		return zero, fmt.Errorf("failed to extract \"%s\": %w", key, ErrKeyNotPresent)
+	} else if narrowed, ok := value.(T); !ok {
+		return zero, fmt.Errorf("failed to extract \"%s\": %w: is %T", key, ErrKeyWrongType, value)
+	} else {
+		return narrowed, nil
+	}
+}
+
+func (o Object) GetString(key string) (string, error) {
+	return getPrimitive[string](o, key)
+}
+
+func (o Object) GetNumber(key string) (uint64, error) {
+	if number, err := getPrimitive[float64](o, key); err != nil {
+		return 0, err
+	} else {
+		return uint64(number), nil
+	}
+}
+
+func (o Object) GetObject(key string) (Object, error) {
+	return getPrimitive[map[string]any](o, key)
+}
+
+func (o Object) GetList(key string) ([]any, error) {
+	if value, err := getPrimitive[any](o, key); err != nil {
+		return nil, err
+	} else if asList, isList := value.([]any); isList {
+		return asList, nil
+	} else {
+		return []any{value}, nil
+	}
+}
+
+func (o Object) GetTime(key string) (time.Time, error) {
+	if value, err := o.GetString(key); err != nil {
+		return time.Time{}, err
+	} else {
+		timestamp, err := time.Parse(time.RFC3339, value)
+		if err != nil {
+			return time.Time{}, fmt.Errorf("failed to parse time \"%s\": %w", key, err)
+		}
+		return timestamp, nil
+	}
+}
+
+func (o Object) GetURL(key string) (*url.URL, error) {
+	if value, err := o.GetString(key); err != nil {
+		return nil, err
+	} else {
+		address, err := url.Parse(value)
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse URL \"%s\": %w", key, err)
+		}
+		return address, nil
+	}
+}
+
+func (o Object) GetMediaType(key string) (*mime.MediaType, error) {
+	if value, err := o.GetString(key); err != nil {
+		return nil, err
+	} else {
+		mediaType, err := mime.Parse(value)
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse mime type \"%s\": %w", key, err)
+		}
+		return mediaType, nil
+	}
+}
+
+/* https://www.w3.org/TR/activitystreams-core/#naturalLanguageValues */
+func (o Object) GetNatural(key string, language string) (string, error) {
+	values, err := o.GetObject(key+"Map")
+	hasMap := true
+	if errors.Is(err, ErrKeyNotPresent) {
+		hasMap = false
+	} else if err != nil {
+		return "", err
+	}
+
+	if hasMap {
+		if value, err := values.GetString(language); err == nil {
+			return value, nil
+		} else if !errors.Is(err, ErrKeyNotPresent) {
+			return "", fmt.Errorf("failed to extract from \"%s\": %w", key+"Map", err)
+		}
+	}
+
+	if value, err := o.GetString(key); err == nil {
+		return value, nil
+	} else if !errors.Is(err, ErrKeyNotPresent) {
+		return "", err
+	}
+
+	if hasMap {
+		if value, err := values.GetString("und"); err == nil {
+			return value, nil
+		} else if !errors.Is(err, ErrKeyNotPresent) {
+			return "", fmt.Errorf("failed to extract from \"%s\": %w", key+"Map", err)
+		}
+	}
+
+	return "", fmt.Errorf("failed to extract natural \"%s\": %w", key, ErrKeyNotPresent)
+}
+
+func (o Object) Has(key string) bool {
+	_, present := o[key]
+	return present
+}
+func (o Object) HasNatural(key string) bool {
+	return o.Has(key) || o.Has(key+"Map")
+}
+
+func (o Object) Render(contentKey string, langKey string, mediaTypeKey string, width int) (string, error) {
+	body, err := o.GetNatural(contentKey, langKey)
+	if err != nil {
+		return "", err
+	}
+	mediaType, err := o.GetMediaType(mediaTypeKey)
+	if err != nil {
+		if errors.Is(err, ErrKeyNotPresent) {
+			mediaType = mime.Default()
+		} else {
+			return "", nil
+		}
+	}
+	return render.Render(body, mediaType.Essence, width)
+}

+ 128 - 0
object/object_test.go

@@ -0,0 +1,128 @@
+package object
+
+import (
+	"testing"
+	"errors"
+	// "encoding/json"
+)
+
+func TestString(t *testing.T) {
+	o := Object {
+		"good": "value",
+		"bad": float64(25),
+		// deliberately absent: "absent": "value",
+	}
+	str, err := o.GetString("good")
+	if err != nil { t.Fatalf("Problem extracting string: %v", err) }
+	if str != "value" { t.Fatalf(`Expected "value" not %v`, str) }
+
+	_, err = o.GetString("bad")
+	if !errors.Is(err, ErrKeyWrongType) { t.Fatalf(`Expected ErrKeyWrongType, not %v`, err) }
+
+	_, err = o.GetString("absent")
+	if !errors.Is(err, ErrKeyNotPresent) { t.Fatalf(`Expected ErrKeyNotPresent, not %v`, err) }
+}
+
+func TestNumber(t *testing.T) {
+	o := Object {
+		"good": float64(25),
+		"bad": "value",
+		// deliberately absent: "absent": "value",
+	}
+	num, err := o.GetNumber("good")
+	if err != nil { t.Fatalf("Problem extracting number: %v", err) }
+	if num != 25 { t.Fatalf(`Expected 25 not %v`, num) }
+
+	_, err = o.GetNumber("bad")
+	if !errors.Is(err, ErrKeyWrongType) { t.Fatalf(`Expected ErrKeyWrongType, not %v`, err) }
+
+	_, err = o.GetNumber("absent")
+	if !errors.Is(err, ErrKeyNotPresent) { t.Fatalf(`Expected ErrKeyNotPresent, not %v`, err) }
+}
+
+func TestObject(t *testing.T) {
+	o := Object {
+		"good": map[string]any{},
+		"bad": "value",
+		// deliberately absent: "absent": "value",
+	}
+	obj, err := o.GetObject("good")
+	if err != nil { t.Fatalf("Problem extracting Object: %v", err) }
+	if len(obj) != 0 { t.Fatalf(`Expected empty map, not %v`, obj) }
+
+	_, err = o.GetObject("bad")
+	if !errors.Is(err, ErrKeyWrongType) { t.Fatalf(`Expected ErrKeyWrongType, not %v`, err) }
+
+	_, err = o.GetObject("absent")
+	if !errors.Is(err, ErrKeyNotPresent) { t.Fatalf(`Expected ErrKeyNotPresent, not %v`, err) }
+}
+
+func TestList(t *testing.T) {
+	o := Object {
+		"multiple": []any{"first", "second"},
+		"single": "one",
+		// deliberately absent: "absent": "value",
+	}
+	list, err := o.GetList("multiple")
+	if err != nil { t.Fatalf("Problem extracting list: %v", err) }
+	if len(list) != 2 { t.Fatalf(`Expected 2 elements, but didn't get them: %v`, list) }
+
+	list, err = o.GetList("single")
+	if err != nil { t.Fatalf("Problem extracting list: %v", err) }
+	if len(list) != 1 { t.Fatalf(`Expected 1 element to auto-convert to list, but didn't: %v`, list) }
+
+	_, err = o.GetList("absent")
+	if !errors.Is(err, ErrKeyNotPresent) { t.Fatalf(`Expected ErrKeyNotPresent, not %v`, err) }
+}
+
+func TestNatural(t *testing.T) {
+	// desired key should have value "target"
+	// language that is targeted is "en"
+	tests := []Object {
+		// TODO: this hasn't been implemented
+		// I will want it to be deterministic, so will need to sort by key and then take the first one
+		// // fall back to first element of map if nothing better
+		// {
+		// 	"contentMap": map[string]any {
+		// 		"fr": "target",
+		// 	},
+		// },
+
+		// use "und" if nothing better
+		{
+			"contentMap": map[string]any {
+				"und": "target",
+				"fr": "ignored",
+			},
+		},
+
+		// use the key itself if nothing better
+		{
+			"content": "target",
+			"contentMap": map[string]any {
+				"und": "ignored",
+				"fr": "ignored",
+			},
+		},
+
+		// use the desired language if possible
+		{
+			"content": "ignored",
+			"contentMap": map[string]any {
+				"en": "target",
+				"und": "ignored",
+				"fr": "ignored",
+			},
+		},
+
+		// use key itself if map is absent
+		{
+			"content": "target",
+		},
+	}
+	for i, test := range tests {
+		response, err := test.GetNatural("content", "en")
+		if err != nil { t.Fatalf("Problem extracting natural in case %v: %v", i, err) }
+		if response != "target" { t.Fatalf(`Expected natural value in case %v to return "target", not %#v`, i, response) }
+	}
+}