Skip to content

Commit fbe0fec

Browse files
authored
Add context-aware string projection for metadata fields (#1)
1 parent d6784f2 commit fbe0fec

4 files changed

Lines changed: 104 additions & 3 deletions

File tree

‎README.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ for _, keyword := range doc.Keywords().Values() {
5555
}
5656
```
5757

58+
`Strings` projects a term into display text, unwrapping `@list`, `@set`, and `@value` and reading `@id` or `name` from objects. It returns scalar spellings in source order and skips empty strings and nulls:
59+
60+
```go
61+
fmt.Println(doc.Strings("keywords"))
62+
fmt.Println(doc.Strings("license"))
63+
```
64+
5865
Values expose `Kind`, `Text`, `Items`, `Fields`, and `Position`. `Items` returns only array elements; `Values` returns array elements or a single scalar or object. A missing field has kind `Missing` and no values, while explicit JSON `Null` remains a value. Numbers retain their spelling and precision. Dates retain their text.
5966

6067
`Author`, `Contributor`, `Maintainer`, `CopyrightHolder`, and `Funder` return ordered agent views, accepting either a single value or an array. `Agent.Kind` distinguishes people, organisations, strings, ID references, roles, and conflicting fields. `Value` preserves each original value; `Get` accesses its fields. `RoleName` and `Agents` expose a role and its nested agents without discarding the wrapper.

‎model.go‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package codemeta
22

3-
func (d *Document) Name() string { return d.Get("name").Text() }
3+
const nameTerm = "name"
4+
5+
func (d *Document) Name() string { return d.Get(nameTerm).Text() }
46
func (d *Document) Description() string { return d.Get("description").Text() }
57
func (d *Document) CodeRepository() Value { return d.Get("codeRepository") }
68
func (d *Document) SoftwareVersion() Value { return d.Get("version") }
@@ -39,7 +41,7 @@ type Agent struct {
3941

4042
func (a Agent) Value() Value { return a.value }
4143
func (a Agent) Get(term string) Value { return resolvedGet(a.value, term, a.terms) }
42-
func (a Agent) Name() string { return a.Get("name").Text() }
44+
func (a Agent) Name() string { return a.Get(nameTerm).Text() }
4345
func (a Agent) GivenName() string { return a.Get("givenName").Text() }
4446
func (a Agent) FamilyName() string { return a.Get("familyName").Text() }
4547
func (a Agent) RoleName() Value { return a.Get("roleName") }
@@ -72,7 +74,7 @@ func (a Agent) Kind() AgentKind {
7274
if personType {
7375
return AgentPerson
7476
}
75-
if orgType || a.Get("name").kind != Missing {
77+
if orgType || a.Get(nameTerm).kind != Missing {
7678
return AgentOrganization
7779
}
7880
return AgentUnknown

‎strings.go‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package codemeta
2+
3+
// Strings projects a term's scalar spelling, object identifiers, or object names
4+
// in source order. It unwraps JSON-LD lists, sets, and value objects and resolves
5+
// keys using each object's context. Empty strings and nulls are skipped. The
6+
// original values remain available through Get.
7+
func (d *Document) Strings(term string) []string {
8+
if d == nil {
9+
return nil
10+
}
11+
var terms map[string]termDefinition
12+
if d.context.usable() {
13+
terms = contextTerms[d.context.version]
14+
}
15+
return stringValues(d.Get(term), terms)
16+
}
17+
18+
func stringValues(value Value, terms map[string]termDefinition) []string {
19+
var values []string
20+
for _, item := range value.Values() {
21+
if item.kind == String || item.kind == Number || item.kind == Boolean {
22+
if item.text != "" {
23+
values = append(values, item.text)
24+
}
25+
continue
26+
}
27+
if item.kind != Object {
28+
continue
29+
}
30+
localTerms := scopedTerms(item, terms)
31+
// An empty identifier means absent, so the search falls through to a name;
32+
// an empty list, set, or value is genuinely empty and stops the search.
33+
for _, key := range []string{keywordList, keywordSet, keywordValue, keywordID, nameTerm} {
34+
field := resolvedGet(item, key, localTerms)
35+
if field.kind == Missing {
36+
continue
37+
}
38+
projected := stringValues(field, localTerms)
39+
values = append(values, projected...)
40+
if key != keywordID || len(projected) != 0 {
41+
break
42+
}
43+
}
44+
}
45+
return values
46+
}

‎strings_test.go‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package codemeta_test
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
7+
"github.com/git-pkgs/codemeta"
8+
)
9+
10+
func TestDocumentStrings(t *testing.T) {
11+
for _, tc := range []struct {
12+
name, input, term string
13+
want []string
14+
}{
15+
{"scalar", `{"keywords":"science"}`, "keywords", []string{"science"}},
16+
{"array", `{"keywords":["science","metadata"]}`, "keywords", []string{"science", "metadata"}},
17+
{"set", `{"keywords":{"@set":["science","metadata"]}}`, "keywords", []string{"science", "metadata"}},
18+
{"list", `{"keywords":{"@list":["science",{"@value":"metadata"}]}}`, "keywords", []string{"science", "metadata"}},
19+
{"id alias", `{"@context":"https://w3id.org/codemeta/3.0","license":{"id":"https://spdx.org/licenses/MIT"}}`, "license", []string{"https://spdx.org/licenses/MIT"}},
20+
{"prefixed names", `{"@context":"https://w3id.org/codemeta/3.0","schema:programmingLanguage":[{"schema:name":"Go"},{"http://schema.org/name":"Ruby"}]}`, "programmingLanguage", []string{"Go", "Ruby"}},
21+
{"identifier first", `{"license":{"@id":"MIT","name":"MIT License"}}`, "license", []string{"MIT"}},
22+
{"empty identifier", `{"license":{"@id":"","name":"MIT License"}}`, "license", []string{"MIT License"}},
23+
{"exact key first", `{"@context":"https://w3id.org/codemeta/3.0","programmingLanguage":{"name":"Go","schema:name":"Ruby"}}`, "programmingLanguage", []string{"Go"}},
24+
{"local context", `{"programmingLanguage":{"@context":"https://w3id.org/codemeta/3.0","schema:name":"Go"}}`, "programmingLanguage", []string{"Go"}},
25+
{"reset context", `{"@context":"https://w3id.org/codemeta/3.0","programmingLanguage":{"@context":null,"schema:name":"Go"}}`, "programmingLanguage", nil},
26+
{"unsupported context", `{"@context":"https://example.org/context","programmingLanguage":{"schema:name":"Go"}}`, "programmingLanguage", nil},
27+
{"scalars", `{"keywords":[null,4,true,"",{},"science"]}`, "keywords", []string{"4", "true", "science"}},
28+
{"number spelling", `{"copyrightYear":2020}`, "copyrightYear", []string{"2020"}},
29+
{"number value object", `{"copyrightYear":{"@value":2020}}`, "copyrightYear", []string{"2020"}},
30+
{"missing", `{}`, "keywords", nil},
31+
} {
32+
t.Run(tc.name, func(t *testing.T) {
33+
doc, err := codemeta.Parse([]byte(tc.input))
34+
if err != nil {
35+
t.Fatal(err)
36+
}
37+
before := doc.Get(tc.term)
38+
if got := doc.Strings(tc.term); !reflect.DeepEqual(got, tc.want) {
39+
t.Fatalf("Strings(%q) = %v, want %v", tc.term, got, tc.want)
40+
}
41+
if !reflect.DeepEqual(doc.Get(tc.term), before) {
42+
t.Fatal("projection changed the source value")
43+
}
44+
})
45+
}
46+
}

0 commit comments

Comments
 (0)