diff --git a/README.md b/README.md index 45461ac..8c2033d 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,13 @@ for _, keyword := range doc.Keywords().Values() { } ``` +`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: + +```go +fmt.Println(doc.Strings("keywords")) +fmt.Println(doc.Strings("license")) +``` + 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. `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. diff --git a/model.go b/model.go index 13c4a40..0fa028e 100644 --- a/model.go +++ b/model.go @@ -1,6 +1,8 @@ package codemeta -func (d *Document) Name() string { return d.Get("name").Text() } +const nameTerm = "name" + +func (d *Document) Name() string { return d.Get(nameTerm).Text() } func (d *Document) Description() string { return d.Get("description").Text() } func (d *Document) CodeRepository() Value { return d.Get("codeRepository") } func (d *Document) SoftwareVersion() Value { return d.Get("version") } @@ -39,7 +41,7 @@ type Agent struct { func (a Agent) Value() Value { return a.value } func (a Agent) Get(term string) Value { return resolvedGet(a.value, term, a.terms) } -func (a Agent) Name() string { return a.Get("name").Text() } +func (a Agent) Name() string { return a.Get(nameTerm).Text() } func (a Agent) GivenName() string { return a.Get("givenName").Text() } func (a Agent) FamilyName() string { return a.Get("familyName").Text() } func (a Agent) RoleName() Value { return a.Get("roleName") } @@ -72,7 +74,7 @@ func (a Agent) Kind() AgentKind { if personType { return AgentPerson } - if orgType || a.Get("name").kind != Missing { + if orgType || a.Get(nameTerm).kind != Missing { return AgentOrganization } return AgentUnknown diff --git a/strings.go b/strings.go new file mode 100644 index 0000000..97d4137 --- /dev/null +++ b/strings.go @@ -0,0 +1,46 @@ +package codemeta + +// Strings projects a term's scalar spelling, object identifiers, or object names +// in source order. It unwraps JSON-LD lists, sets, and value objects and resolves +// keys using each object's context. Empty strings and nulls are skipped. The +// original values remain available through Get. +func (d *Document) Strings(term string) []string { + if d == nil { + return nil + } + var terms map[string]termDefinition + if d.context.usable() { + terms = contextTerms[d.context.version] + } + return stringValues(d.Get(term), terms) +} + +func stringValues(value Value, terms map[string]termDefinition) []string { + var values []string + for _, item := range value.Values() { + if item.kind == String || item.kind == Number || item.kind == Boolean { + if item.text != "" { + values = append(values, item.text) + } + continue + } + if item.kind != Object { + continue + } + localTerms := scopedTerms(item, terms) + // An empty identifier means absent, so the search falls through to a name; + // an empty list, set, or value is genuinely empty and stops the search. + for _, key := range []string{keywordList, keywordSet, keywordValue, keywordID, nameTerm} { + field := resolvedGet(item, key, localTerms) + if field.kind == Missing { + continue + } + projected := stringValues(field, localTerms) + values = append(values, projected...) + if key != keywordID || len(projected) != 0 { + break + } + } + } + return values +} diff --git a/strings_test.go b/strings_test.go new file mode 100644 index 0000000..f970092 --- /dev/null +++ b/strings_test.go @@ -0,0 +1,46 @@ +package codemeta_test + +import ( + "reflect" + "testing" + + "github.com/git-pkgs/codemeta" +) + +func TestDocumentStrings(t *testing.T) { + for _, tc := range []struct { + name, input, term string + want []string + }{ + {"scalar", `{"keywords":"science"}`, "keywords", []string{"science"}}, + {"array", `{"keywords":["science","metadata"]}`, "keywords", []string{"science", "metadata"}}, + {"set", `{"keywords":{"@set":["science","metadata"]}}`, "keywords", []string{"science", "metadata"}}, + {"list", `{"keywords":{"@list":["science",{"@value":"metadata"}]}}`, "keywords", []string{"science", "metadata"}}, + {"id alias", `{"@context":"https://w3id.org/codemeta/3.0","license":{"id":"https://spdx.org/licenses/MIT"}}`, "license", []string{"https://spdx.org/licenses/MIT"}}, + {"prefixed names", `{"@context":"https://w3id.org/codemeta/3.0","schema:programmingLanguage":[{"schema:name":"Go"},{"http://schema.org/name":"Ruby"}]}`, "programmingLanguage", []string{"Go", "Ruby"}}, + {"identifier first", `{"license":{"@id":"MIT","name":"MIT License"}}`, "license", []string{"MIT"}}, + {"empty identifier", `{"license":{"@id":"","name":"MIT License"}}`, "license", []string{"MIT License"}}, + {"exact key first", `{"@context":"https://w3id.org/codemeta/3.0","programmingLanguage":{"name":"Go","schema:name":"Ruby"}}`, "programmingLanguage", []string{"Go"}}, + {"local context", `{"programmingLanguage":{"@context":"https://w3id.org/codemeta/3.0","schema:name":"Go"}}`, "programmingLanguage", []string{"Go"}}, + {"reset context", `{"@context":"https://w3id.org/codemeta/3.0","programmingLanguage":{"@context":null,"schema:name":"Go"}}`, "programmingLanguage", nil}, + {"unsupported context", `{"@context":"https://example.org/context","programmingLanguage":{"schema:name":"Go"}}`, "programmingLanguage", nil}, + {"scalars", `{"keywords":[null,4,true,"",{},"science"]}`, "keywords", []string{"4", "true", "science"}}, + {"number spelling", `{"copyrightYear":2020}`, "copyrightYear", []string{"2020"}}, + {"number value object", `{"copyrightYear":{"@value":2020}}`, "copyrightYear", []string{"2020"}}, + {"missing", `{}`, "keywords", nil}, + } { + t.Run(tc.name, func(t *testing.T) { + doc, err := codemeta.Parse([]byte(tc.input)) + if err != nil { + t.Fatal(err) + } + before := doc.Get(tc.term) + if got := doc.Strings(tc.term); !reflect.DeepEqual(got, tc.want) { + t.Fatalf("Strings(%q) = %v, want %v", tc.term, got, tc.want) + } + if !reflect.DeepEqual(doc.Get(tc.term), before) { + t.Fatal("projection changed the source value") + } + }) + } +}