From 8dc074404173600fa53d49752922a4028fe1f4a5 Mon Sep 17 00:00:00 2001 From: "@s.roertgen" Date: Mon, 27 Oct 2025 22:13:35 +0100 Subject: [PATCH] use new amb nip --- typesense30142/nostr_amb.go | 1030 +++++++++++++++++++++--------- typesense30142/nostr_amb_test.go | 382 ++++++----- typesense30142/types.go | 52 +- 3 files changed, 1004 insertions(+), 460 deletions(-) diff --git a/typesense30142/nostr_amb.go b/typesense30142/nostr_amb.go index d2fac80..c417de1 100644 --- a/typesense30142/nostr_amb.go +++ b/typesense30142/nostr_amb.go @@ -3,10 +3,491 @@ package typesense30142 import ( "encoding/json" "fmt" + "strings" "github.com/nbd-wtf/go-nostr" ) +// tagGroup represents a grouped tag with its full key and values +type tagGroup struct { + fullKey string + values []string +} + +// groupTagsByBase groups tags by their base key (before first ':') +// Example: "creator:name" and "creator:affiliation:name" both group under "creator" +func groupTagsByBase(tags nostr.Tags) map[string][]tagGroup { + grouped := make(map[string][]tagGroup) + + for _, tag := range tags { + if len(tag) < 2 { + continue + } + + key := tag[0] + values := tag[1:] + + // Skip special Nostr-native tags (handled separately) + if key == "d" || key == "t" || key == "p" || key == "a" { + continue + } + + // Get base key (before first ':') + baseKey := key + if idx := strings.Index(key, ":"); idx != -1 { + baseKey = key[:idx] + } + + grouped[baseKey] = append(grouped[baseKey], tagGroup{ + fullKey: key, + values: values, + }) + } + + return grouped +} + +// parseNestedValue reconstructs a nested value from a colon-delimited key +// Example: "creator:affiliation:name" -> sets value in nested structure +func parseNestedValue(result map[string]interface{}, key string, value string) { + parts := strings.Split(key, ":") + + // Navigate/create nested structure + current := result + for i := 0; i < len(parts)-1; i++ { + part := parts[i] + if _, exists := current[part]; !exists { + current[part] = make(map[string]interface{}) + } + current = current[part].(map[string]interface{}) + } + + // Set the final value + lastKey := parts[len(parts)-1] + current[lastKey] = value +} + +// separateArrayInstances detects multiple instances in array fields +// When we see a repeated base property (e.g., second "name" in creator:name), +// it indicates a new array item +func separateArrayInstances(tags []tagGroup) []map[string]interface{} { + if len(tags) == 0 { + return nil + } + + instances := []map[string]interface{}{} + currentInstance := make(map[string]interface{}) + seenKeys := make(map[string]bool) + + for _, tag := range tags { + // Get the immediate child key after base + // E.g., "creator:name" -> "name", "creator:affiliation:name" -> "affiliation" + parts := strings.Split(tag.fullKey, ":") + if len(parts) < 2 { + continue + } + + immediateKey := parts[1] + + // If we've seen this immediate key before and current instance is not empty, + // we're starting a new array item + if seenKeys[immediateKey] && len(currentInstance) > 0 { + instances = append(instances, currentInstance) + currentInstance = make(map[string]interface{}) + seenKeys = make(map[string]bool) + } + + seenKeys[immediateKey] = true + + // Remove base key from the path + subKey := strings.Join(parts[1:], ":") + parseNestedValue(currentInstance, subKey, tag.values[0]) + } + + // Don't forget the last instance + if len(currentInstance) > 0 { + instances = append(instances, currentInstance) + } + + return instances +} + +// handleTTags extracts keywords from Nostr-native 't' tags +func handleTTags(tags nostr.Tags) []string { + var keywords []string + for _, tag := range tags { + if len(tag) >= 2 && tag[0] == "t" { + keywords = append(keywords, tag[1]) + } + } + return keywords +} + +// handlePTags extracts creator and contributor pubkeys from 'p' tags +func handlePTags(tags nostr.Tags) (creatorPubkeys []string, contributorPubkeys []string) { + for _, tag := range tags { + if len(tag) >= 4 && tag[0] == "p" { + pubkey := tag[1] + marker := tag[3] + + switch marker { + case "creator": + creatorPubkeys = append(creatorPubkeys, pubkey) + case "contributor": + contributorPubkeys = append(contributorPubkeys, pubkey) + } + } + } + return +} + +// handleATags extracts addressable event references with relation markers +func handleATags(tags nostr.Tags) (isBasedOn, isPartOf, hasPart []string) { + for _, tag := range tags { + if len(tag) >= 4 && tag[0] == "a" { + eventAddr := tag[1] + marker := tag[3] + + switch marker { + case "isBasedOn": + isBasedOn = append(isBasedOn, eventAddr) + case "isPartOf": + isPartOf = append(isPartOf, eventAddr) + case "hasPart": + hasPart = append(hasPart, eventAddr) + } + } + } + return +} + +// Field-specific parsers for complex nested structures + +// parseControlledVocabularyArray parses an array of ControlledVocabulary items from grouped tags +func parseControlledVocabularyArray(instances []map[string]interface{}) []ControlledVocabulary { + var result []ControlledVocabulary + for _, inst := range instances { + cv := ControlledVocabulary{} + if id, ok := inst["id"].(string); ok { + cv.ID = id + } + if prefLabel, ok := inst["prefLabel"].(string); ok { + cv.PrefLabel = prefLabel + } + if inLanguage, ok := inst["inLanguage"].(string); ok { + cv.InLanguage = inLanguage + } + if typ, ok := inst["type"].(string); ok { + cv.Type = typ + } + result = append(result, cv) + } + return result +} + +// parseCreators parses creator array from grouped tags and p tag pubkeys +func parseCreators(instances []map[string]interface{}, pubkeys []string) []*Creator { + var creators []*Creator + + // First, add creators with pubkeys from p tags + for _, pubkey := range pubkeys { + creators = append(creators, &Creator{ + BaseEntity: BaseEntity{ + ID: pubkey, + }, + }) + } + + // Then parse creators from flattened tags + for _, inst := range instances { + creator := &Creator{} + + if id, ok := inst["id"].(string); ok { + creator.ID = id + } + if name, ok := inst["name"].(string); ok { + creator.Name = name + } + if typ, ok := inst["type"].(string); ok { + creator.Type = typ + } + if honorificPrefix, ok := inst["honorificPrefix"].(string); ok { + creator.HonoricPrefix = honorificPrefix + } + + // Parse affiliation if present + if affData, ok := inst["affiliation"].(map[string]interface{}); ok { + aff := &Affiliation{} + if id, ok := affData["id"].(string); ok { + aff.ID = id + } + if name, ok := affData["name"].(string); ok { + aff.Name = name + } + if typ, ok := affData["type"].(string); ok { + aff.Type = typ + } + creator.Affiliation = aff + } + + creators = append(creators, creator) + } + + return creators +} + +// parseContributors parses contributor array from grouped tags and p tag pubkeys +func parseContributors(instances []map[string]interface{}, pubkeys []string) []*Contributor { + var contributors []*Contributor + + // First, add contributors with pubkeys from p tags + for _, pubkey := range pubkeys { + contributors = append(contributors, &Contributor{ + BaseEntity: BaseEntity{ + ID: pubkey, + }, + }) + } + + // Then parse contributors from flattened tags + for _, inst := range instances { + contributor := &Contributor{} + + if id, ok := inst["id"].(string); ok { + contributor.ID = id + } + if name, ok := inst["name"].(string); ok { + contributor.Name = name + } + if typ, ok := inst["type"].(string); ok { + contributor.Type = typ + } + if honorificPrefix, ok := inst["honorificPrefix"].(string); ok { + contributor.HonoricPrefix = honorificPrefix + } + + // Parse affiliation if present + if affData, ok := inst["affiliation"].(map[string]interface{}); ok { + aff := &Affiliation{} + if id, ok := affData["id"].(string); ok { + aff.ID = id + } + if name, ok := affData["name"].(string); ok { + aff.Name = name + } + if typ, ok := affData["type"].(string); ok { + aff.Type = typ + } + contributor.Affiliation = aff + } + + contributors = append(contributors, contributor) + } + + return contributors +} + +// parseBaseEntityArray parses an array of BaseEntity items +func parseBaseEntityArray(instances []map[string]interface{}) []BaseEntity { + var result []BaseEntity + for _, inst := range instances { + entity := BaseEntity{} + if id, ok := inst["id"].(string); ok { + entity.ID = id + } + if name, ok := inst["name"].(string); ok { + entity.Name = name + } + if typ, ok := inst["type"].(string); ok { + entity.Type = typ + } + result = append(result, entity) + } + return result +} + +// parseTrailers parses trailer array from grouped tags +func parseTrailers(instances []map[string]interface{}) []*Trailer { + var trailers []*Trailer + for _, inst := range instances { + trailer := &Trailer{} + if contentUrl, ok := inst["contentUrl"].(string); ok { + trailer.ContentUrl = contentUrl + } + if typ, ok := inst["type"].(string); ok { + trailer.Type = typ + } + if encodingFormat, ok := inst["encodingFormat"].(string); ok { + trailer.EncodingFormat = encodingFormat + } + if contentSize, ok := inst["contentSize"].(string); ok { + trailer.ContentSize = contentSize + } + if sha256, ok := inst["sha256"].(string); ok { + trailer.Sha256 = sha256 + } + if embedUrl, ok := inst["embedUrl"].(string); ok { + trailer.EmbedUrl = embedUrl + } + if bitrate, ok := inst["bitrate"].(string); ok { + trailer.Bitrate = bitrate + } + trailers = append(trailers, trailer) + } + return trailers +} + +// parseEncodings parses encoding array from grouped tags +func parseEncodings(instances []map[string]interface{}) []*Encoding { + var encodings []*Encoding + for _, inst := range instances { + encoding := &Encoding{} + if typ, ok := inst["type"].(string); ok { + encoding.Type = typ + } + if contentUrl, ok := inst["contentUrl"].(string); ok { + encoding.ContentUrl = contentUrl + } + if embedUrl, ok := inst["embedUrl"].(string); ok { + encoding.EmbedUrl = embedUrl + } + if encodingFormat, ok := inst["encodingFormat"].(string); ok { + encoding.EncodingFormat = encodingFormat + } + if contentSize, ok := inst["contentSize"].(string); ok { + encoding.ContentSize = contentSize + } + if sha256, ok := inst["sha256"].(string); ok { + encoding.Sha256 = sha256 + } + if bitrate, ok := inst["bitrate"].(string); ok { + encoding.Bitrate = bitrate + } + encodings = append(encodings, encoding) + } + return encodings +} + +// parseCaptions parses caption array from grouped tags +func parseCaptions(instances []map[string]interface{}) []*Caption { + var captions []*Caption + for _, inst := range instances { + caption := &Caption{} + if id, ok := inst["id"].(string); ok { + caption.ID = id + } + if typ, ok := inst["type"].(string); ok { + caption.Type = typ + } + if encodingFormat, ok := inst["encodingFormat"].(string); ok { + caption.EncodingFormat = encodingFormat + } + if inLanguage, ok := inst["inLanguage"].(string); ok { + caption.InLanguage = inLanguage + } + captions = append(captions, caption) + } + return captions +} + +// parseMainEntityOfPages parses mainEntityOfPage array from grouped tags +func parseMainEntityOfPages(instances []map[string]interface{}) []*MainEntityOfPage { + var pages []*MainEntityOfPage + for _, inst := range instances { + page := &MainEntityOfPage{} + if id, ok := inst["id"].(string); ok { + page.ID = id + } + if typ, ok := inst["type"].(string); ok { + page.Type = typ + } + if dateCreated, ok := inst["dateCreated"].(string); ok { + page.DateCreated = dateCreated + } + if dateModified, ok := inst["dateModified"].(string); ok { + page.DateModified = dateModified + } + + // Parse provider if present + if providerData, ok := inst["provider"].(map[string]interface{}); ok { + provider := &MainEntityProvider{} + if id, ok := providerData["id"].(string); ok { + provider.ID = id + } + if name, ok := providerData["name"].(string); ok { + provider.Name = name + } + if typ, ok := providerData["type"].(string); ok { + provider.Type = typ + } + page.Provider = provider + } + + pages = append(pages, page) + } + return pages +} + +// parseLicense parses a single license from grouped tags +func parseLicense(instances []map[string]interface{}) *License { + if len(instances) == 0 { + return nil + } + inst := instances[0] + license := &License{} + if id, ok := inst["id"].(string); ok { + license.ID = id + } + if name, ok := inst["name"].(string); ok { + license.Name = name + } + return license +} + +// parseConditionsOfAccess parses a single conditionsOfAccess from grouped tags +func parseConditionsOfAccess(instances []map[string]interface{}) *ConditionsOfAccess { + if len(instances) == 0 { + return nil + } + inst := instances[0] + coa := &ConditionsOfAccess{} + if id, ok := inst["id"].(string); ok { + coa.ID = id + } + if prefLabel, ok := inst["prefLabel"].(string); ok { + coa.PrefLabel = prefLabel + } + if inLanguage, ok := inst["inLanguage"].(string); ok { + coa.InLanguage = inLanguage + } + if typ, ok := inst["type"].(string); ok { + coa.Type = typ + } + return coa +} + +// parseInteractivityType parses a single interactivityType from grouped tags +func parseInteractivityType(instances []map[string]interface{}) *InteractivityType { + if len(instances) == 0 { + return nil + } + inst := instances[0] + it := &InteractivityType{} + if id, ok := inst["id"].(string); ok { + it.ID = id + } + if prefLabel, ok := inst["prefLabel"].(string); ok { + it.PrefLabel = prefLabel + } + if inLanguage, ok := inst["inLanguage"].(string); ok { + it.InLanguage = inLanguage + } + if typ, ok := inst["type"].(string); ok { + it.Type = typ + } + return it +} + // converts a nostr event to stringified JSON func eventToStringifiedJSON(event *nostr.Event) (string, error) { jsonData, err := json.Marshal(event) @@ -18,7 +499,7 @@ func eventToStringifiedJSON(event *nostr.Event) (string, error) { return jsonString, err } -// NostrToAMB converts a Nostr event of kind 30142 to AMB metadata +// NostrToAMB converts a Nostr event of kind 30142 to AMB metadata using the new flattened tag format func NostrToAMB(event *nostr.Event) (*AMBMetadata, error) { if event == nil { return nil, fmt.Errorf("cannot convert nil event") @@ -28,6 +509,8 @@ func NostrToAMB(event *nostr.Event) (*AMBMetadata, error) { if err != nil { return nil, fmt.Errorf("error converting event to JSON: %w", err) } + + // Initialize AMB metadata amb := &AMBMetadata{ Type: []string{"LearningResource"}, NostrMetadata: NostrMetadata{ @@ -39,306 +522,268 @@ func NostrToAMB(event *nostr.Event) (*AMBMetadata, error) { EventSig: event.Sig, EventRaw: eventRaw, }, - About: []*About{}, - Keywords: []string{}, - InLanguage: []string{}, - Creator: []*Creator{}, - Contributor: []*Contributor{}, - Publisher: []*Publisher{}, - Funder: []*Funder{}, - LearningResourceType: []*LearningResourceType{}, - Audience: []*Audience{}, - Teaches: []*Teaches{}, - Assesses: []*Assesses{}, - CompetencyRequired: []*CompetencyRequired{}, - EducationalLevel: []*EducationalLevel{}, - IsBasedOn: []*IsBasedOn{}, - IsPartOf: []*IsPartOf{}, - HasPart: []*HasPart{}, - Trailer: []*Trailer{}, } + // Phase 1: Extract Nostr-native tags + // Handle 'd' tag for _, tag := range event.Tags { - if len(tag) < 2 { - continue + if len(tag) >= 2 && tag[0] == "d" { + amb.D = tag[1] + amb.ID = event.ID + break } + } - switch tag[0] { - case "d": - if len(tag) >= 2 { - amb.D = tag[1] - amb.ID = event.ID - } - case "type": - if len(tag) >= 2 { - amb.Type = tag[1:] - } + // Handle 't' tags (keywords) + amb.Keywords = handleTTags(event.Tags) - case "name": - if len(tag) >= 2 { - amb.Name = tag[1] - } - case "description": - if len(tag) >= 2 { - amb.Description = tag[1] - } - case "creator": - if len(tag) >= 3 { - creator := &Creator{ - BaseEntity: BaseEntity{ - ID: tag[1], - Name: tag[2], - }, - Affiliation: &Affiliation{}, - } - if len(tag) >= 4 { - creator.Type = tag[3] - } - if len(tag) >= 5 { - creator.Affiliation.Name = tag[4] - } - if len(tag) >= 6 { - creator.Affiliation.Type = tag[5] - } - if len(tag) >= 7 { - creator.Affiliation.ID = tag[6] - } - amb.Creator = append(amb.Creator, creator) - } - case "image": - if len(tag) >= 2 { - amb.Image = tag[1] - } - case "about": - if len(tag) >= 4 { - subject := &About{ - ControlledVocabulary: ControlledVocabulary{ - ID: tag[1], - PrefLabel: tag[2], - InLanguage: tag[3], - }, - } - if len(tag) >= 5 { - subject.Type = tag[4] - } - amb.About = append(amb.About, subject) - } - case "learningResourceType": - if len(tag) >= 4 { - lrt := &LearningResourceType{ - ControlledVocabulary: ControlledVocabulary{ - ID: tag[1], - PrefLabel: tag[2], - InLanguage: tag[3], - }, - } - amb.LearningResourceType = append(amb.LearningResourceType, lrt) - } - case "inLanguage": - if len(tag) >= 2 { - amb.InLanguage = append(amb.InLanguage, tag[1]) - } - case "keywords": - if len(tag) >= 2 { - amb.Keywords = tag[1:] - } - case "license": - if len(tag) >= 3 { - amb.License = &License{ - ID: tag[1], - Name: tag[2], - } - } - case "datePublished": - if len(tag) >= 2 { - amb.DatePublished = tag[1] - } - case "dateCreated": - if len(tag) >= 2 { - amb.DateCreated = tag[1] - } - case "dateModified": - if len(tag) >= 2 { - amb.DateModified = tag[1] - } - case "publisher": - if len(tag) >= 3 { - publisher := &Publisher{ - BaseEntity: BaseEntity{ - ID: tag[1], - Name: tag[2], - }, - } - if len(tag) >= 4 { - publisher.Type = tag[3] - } - amb.Publisher = append(amb.Publisher, publisher) - } - case "contributor": - if len(tag) >= 4 { - contributor := &Contributor{ - BaseEntity: BaseEntity{ - ID: tag[1], - Name: tag[2], - Type: tag[3], - }, - Affiliation: &Affiliation{}, - } - if len(tag) >= 5 { - contributor.Affiliation.Name = tag[4] - } - if len(tag) >= 6 { - contributor.Affiliation.Type = tag[5] - } - if len(tag) >= 7 { - contributor.Affiliation.ID = tag[6] - } - amb.Contributor = append(amb.Contributor, contributor) - } - case "funder": - if len(tag) >= 3 { - funder := &Funder{ - BaseEntity: BaseEntity{ - ID: tag[1], - Name: tag[2], - }, - } - if len(tag) >= 4 { - funder.Type = tag[3] - } - amb.Funder = append(amb.Funder, funder) - } - case "isAccessibleForFree": - if len(tag) >= 2 && (tag[1] == "true" || tag[1] == "1") { - amb.IsAccessibleForFree = true - } - case "audience": - if len(tag) >= 4 { - audience := &Audience{ - ControlledVocabulary: ControlledVocabulary{ - ID: tag[1], - PrefLabel: tag[2], - InLanguage: tag[3], - }, - } - amb.Audience = append(amb.Audience, audience) - } - case "duration": - if len(tag) >= 2 { - amb.Duration = tag[1] - } - case "conditionsOfAccess": - if len(tag) == 4 { - conditionsOfAccess := &ConditionsOfAccess{ - ControlledVocabulary: ControlledVocabulary{ - ID: tag[1], - PrefLabel: tag[2], - InLanguage: tag[3], - }, - } - amb.ConditionsOfAccess = conditionsOfAccess - } - case "teaches": - if len(tag) >= 3 { - teaches := &Teaches{ - ControlledVocabulary: ControlledVocabulary{ - ID: tag[1], - PrefLabel: tag[2], - InLanguage: tag[3], - }, - } - amb.Teaches = append(amb.Teaches, teaches) - } - case "assesses": - if len(tag) >= 3 { - assesses := &Assesses{ - ControlledVocabulary: ControlledVocabulary{ - ID: tag[1], - PrefLabel: tag[2], - InLanguage: tag[3], - }, - } - amb.Assesses = append(amb.Assesses, assesses) - } - case "competencyRequired": - if len(tag) >= 3 { - competencyRequired := &CompetencyRequired{ - ControlledVocabulary: ControlledVocabulary{ - ID: tag[1], - PrefLabel: tag[2], - InLanguage: tag[3], - }, - } - amb.CompetencyRequired = append(amb.CompetencyRequired, competencyRequired) - } - case "educationalLevel": - if len(tag) >= 3 { - educationalLevel := &EducationalLevel{ - ControlledVocabulary: ControlledVocabulary{ - ID: tag[1], - PrefLabel: tag[2], - InLanguage: tag[3], - }, - } - amb.EducationalLevel = append(amb.EducationalLevel, educationalLevel) - } - case "interactivityType": - if len(tag) >= 3 { - interactivityType := &InteractivityType{ - ControlledVocabulary: ControlledVocabulary{ - ID: tag[1], - PrefLabel: tag[2], - InLanguage: tag[3], - }, - } - amb.InteractivityType = interactivityType - } - case "isBasedOn": - if len(tag) >= 3 { - isBasedOn := &IsBasedOn{ - ID: tag[1], - Name: tag[2], - } - amb.IsBasedOn = append(amb.IsBasedOn, isBasedOn) - } - case "isPartOf": - if len(tag) >= 4 { - isPartOf := &IsPartOf{ - BaseEntity: BaseEntity{ - ID: tag[1], - Name: tag[2], - Type: tag[3], - }, - } - amb.IsPartOf = append(amb.IsPartOf, isPartOf) - } - case "hasPart": - if len(tag) >= 4 { - hasPart := &HasPart{ - BaseEntity: BaseEntity{ - ID: tag[1], - Name: tag[2], - Type: tag[3], - }, - } - amb.HasPart = append(amb.HasPart, hasPart) - } - case "trailer": - if len(tag) >= 8 { - trailer := &Trailer{ - ContentUrl: tag[1], - Type: tag[2], - EncodingFormat: tag[3], - ContentSize: tag[4], - Sha256: tag[5], - EmbedUrl: tag[6], - Bitrate: tag[7], - } - amb.Trailer = append(amb.Trailer, trailer) - } + // Handle 'p' tags (creator/contributor pubkeys) + creatorPubkeys, contributorPubkeys := handlePTags(event.Tags) + + // Handle 'a' tags (addressable event relations) + isBasedOnAddrs, isPartOfAddrs, hasPartAddrs := handleATags(event.Tags) + + // Phase 2: Group remaining flattened tags by base key + grouped := groupTagsByBase(event.Tags) + + // Phase 3: Parse simple fields (non-nested) + if tags, ok := grouped["name"]; ok && len(tags) > 0 { + amb.Name = tags[0].values[0] + } + if tags, ok := grouped["description"]; ok && len(tags) > 0 { + amb.Description = tags[0].values[0] + } + if tags, ok := grouped["image"]; ok && len(tags) > 0 { + amb.Image = tags[0].values[0] + } + if tags, ok := grouped["duration"]; ok && len(tags) > 0 { + amb.Duration = tags[0].values[0] + } + if tags, ok := grouped["dateCreated"]; ok && len(tags) > 0 { + amb.DateCreated = tags[0].values[0] + } + if tags, ok := grouped["datePublished"]; ok && len(tags) > 0 { + amb.DatePublished = tags[0].values[0] + } + if tags, ok := grouped["dateModified"]; ok && len(tags) > 0 { + amb.DateModified = tags[0].values[0] + } + if tags, ok := grouped["isAccessibleForFree"]; ok && len(tags) > 0 { + amb.IsAccessibleForFree = (tags[0].values[0] == "true" || tags[0].values[0] == "1") + } + + // Parse type (can have multiple values) + if tags, ok := grouped["type"]; ok { + amb.Type = []string{} + for _, tag := range tags { + amb.Type = append(amb.Type, tag.values[0]) } } + // Parse inLanguage (can have multiple values) + if tags, ok := grouped["inLanguage"]; ok { + for _, tag := range tags { + amb.InLanguage = append(amb.InLanguage, tag.values[0]) + } + } + + // Phase 4: Parse complex nested fields using helper functions + + // About (array of ControlledVocabulary) + if tags, ok := grouped["about"]; ok { + instances := separateArrayInstances(tags) + cvArray := parseControlledVocabularyArray(instances) + for _, cv := range cvArray { + amb.About = append(amb.About, &About{ControlledVocabulary: cv}) + } + } + + // LearningResourceType (array of ControlledVocabulary) + if tags, ok := grouped["learningResourceType"]; ok { + instances := separateArrayInstances(tags) + cvArray := parseControlledVocabularyArray(instances) + for _, cv := range cvArray { + amb.LearningResourceType = append(amb.LearningResourceType, &LearningResourceType{ControlledVocabulary: cv}) + } + } + + // Audience (array of ControlledVocabulary) + if tags, ok := grouped["audience"]; ok { + instances := separateArrayInstances(tags) + cvArray := parseControlledVocabularyArray(instances) + for _, cv := range cvArray { + amb.Audience = append(amb.Audience, &Audience{ControlledVocabulary: cv}) + } + } + + // Teaches (array of ControlledVocabulary) + if tags, ok := grouped["teaches"]; ok { + instances := separateArrayInstances(tags) + cvArray := parseControlledVocabularyArray(instances) + for _, cv := range cvArray { + amb.Teaches = append(amb.Teaches, &Teaches{ControlledVocabulary: cv}) + } + } + + // Assesses (array of ControlledVocabulary) + if tags, ok := grouped["assesses"]; ok { + instances := separateArrayInstances(tags) + cvArray := parseControlledVocabularyArray(instances) + for _, cv := range cvArray { + amb.Assesses = append(amb.Assesses, &Assesses{ControlledVocabulary: cv}) + } + } + + // CompetencyRequired (array of ControlledVocabulary) + if tags, ok := grouped["competencyRequired"]; ok { + instances := separateArrayInstances(tags) + cvArray := parseControlledVocabularyArray(instances) + for _, cv := range cvArray { + amb.CompetencyRequired = append(amb.CompetencyRequired, &CompetencyRequired{ControlledVocabulary: cv}) + } + } + + // EducationalLevel (array of ControlledVocabulary) + if tags, ok := grouped["educationalLevel"]; ok { + instances := separateArrayInstances(tags) + cvArray := parseControlledVocabularyArray(instances) + for _, cv := range cvArray { + amb.EducationalLevel = append(amb.EducationalLevel, &EducationalLevel{ControlledVocabulary: cv}) + } + } + + // InteractivityType (single ControlledVocabulary) + if tags, ok := grouped["interactivityType"]; ok { + instances := separateArrayInstances(tags) + amb.InteractivityType = parseInteractivityType(instances) + } + + // ConditionsOfAccess (single ControlledVocabulary) + if tags, ok := grouped["conditionsOfAccess"]; ok { + instances := separateArrayInstances(tags) + amb.ConditionsOfAccess = parseConditionsOfAccess(instances) + } + + // License (single object) + if tags, ok := grouped["license"]; ok { + instances := separateArrayInstances(tags) + amb.License = parseLicense(instances) + } + + // Creator (array with potential p tag pubkeys) + if tags, ok := grouped["creator"]; ok { + instances := separateArrayInstances(tags) + amb.Creator = parseCreators(instances, creatorPubkeys) + } else if len(creatorPubkeys) > 0 { + // Only p tags, no flattened creator tags + amb.Creator = parseCreators(nil, creatorPubkeys) + } + + // Contributor (array with potential p tag pubkeys) + if tags, ok := grouped["contributor"]; ok { + instances := separateArrayInstances(tags) + amb.Contributor = parseContributors(instances, contributorPubkeys) + } else if len(contributorPubkeys) > 0 { + // Only p tags, no flattened contributor tags + amb.Contributor = parseContributors(nil, contributorPubkeys) + } + + // Publisher (array of BaseEntity) + if tags, ok := grouped["publisher"]; ok { + instances := separateArrayInstances(tags) + entities := parseBaseEntityArray(instances) + for _, entity := range entities { + amb.Publisher = append(amb.Publisher, &Publisher{BaseEntity: entity}) + } + } + + // Funder (array of BaseEntity) + if tags, ok := grouped["funder"]; ok { + instances := separateArrayInstances(tags) + entities := parseBaseEntityArray(instances) + for _, entity := range entities { + amb.Funder = append(amb.Funder, &Funder{BaseEntity: entity}) + } + } + + // IsBasedOn (array, mixing a tags and flattened) + if tags, ok := grouped["isBasedOn"]; ok { + instances := separateArrayInstances(tags) + for _, inst := range instances { + isBasedOn := &IsBasedOn{} + if id, ok := inst["id"].(string); ok { + isBasedOn.ID = id + } + if name, ok := inst["name"].(string); ok { + isBasedOn.Name = name + } + if typ, ok := inst["type"].(string); ok { + isBasedOn.Type = typ + } + amb.IsBasedOn = append(amb.IsBasedOn, isBasedOn) + } + } + // Add addressable event references from a tags + for _, addr := range isBasedOnAddrs { + amb.IsBasedOn = append(amb.IsBasedOn, &IsBasedOn{ + ID: addr, + }) + } + + // IsPartOf (array, mixing a tags and flattened) + if tags, ok := grouped["isPartOf"]; ok { + instances := separateArrayInstances(tags) + entities := parseBaseEntityArray(instances) + for _, entity := range entities { + amb.IsPartOf = append(amb.IsPartOf, &IsPartOf{BaseEntity: entity}) + } + } + // Add addressable event references from a tags + for _, addr := range isPartOfAddrs { + amb.IsPartOf = append(amb.IsPartOf, &IsPartOf{ + BaseEntity: BaseEntity{ID: addr}, + }) + } + + // HasPart (array, mixing a tags and flattened) + if tags, ok := grouped["hasPart"]; ok { + instances := separateArrayInstances(tags) + entities := parseBaseEntityArray(instances) + for _, entity := range entities { + amb.HasPart = append(amb.HasPart, &HasPart{BaseEntity: entity}) + } + } + // Add addressable event references from a tags + for _, addr := range hasPartAddrs { + amb.HasPart = append(amb.HasPart, &HasPart{ + BaseEntity: BaseEntity{ID: addr}, + }) + } + + // Trailer (array of MediaObject) + if tags, ok := grouped["trailer"]; ok { + instances := separateArrayInstances(tags) + amb.Trailer = parseTrailers(instances) + } + + // Encoding (array of MediaObject) + if tags, ok := grouped["encoding"]; ok { + instances := separateArrayInstances(tags) + amb.Encoding = parseEncodings(instances) + } + + // Caption (array of MediaObject) + if tags, ok := grouped["caption"]; ok { + instances := separateArrayInstances(tags) + amb.Caption = parseCaptions(instances) + } + + // MainEntityOfPage (array of WebPage) + if tags, ok := grouped["mainEntityOfPage"]; ok { + instances := separateArrayInstances(tags) + amb.MainEntityOfPage = parseMainEntityOfPages(instances) + } + return amb, nil } @@ -351,4 +796,3 @@ func StringifiedJSONToNostrEvent(jsonString string) (nostr.Event, error) { } return event, nil } - diff --git a/typesense30142/nostr_amb_test.go b/typesense30142/nostr_amb_test.go index 4a01b99..5a154e2 100644 --- a/typesense30142/nostr_amb_test.go +++ b/typesense30142/nostr_amb_test.go @@ -7,7 +7,6 @@ import ( "github.com/stretchr/testify/assert" ) - func TestNostrToAmbEvent(t *testing.T) { assert := assert.New(t) sk := nostr.GeneratePrivateKey() @@ -38,7 +37,6 @@ func TestNostrToAmbEvent(t *testing.T) { } - // Helper function to create a test event with a specific tag func createTestEvent(tags nostr.Tags) *nostr.Event { sk := nostr.GeneratePrivateKey() @@ -54,24 +52,24 @@ func createTestEvent(tags nostr.Tags) *nostr.Event { func TestNostrToAMB_BasicFields(t *testing.T) { assert := assert.New(t) - + tags := nostr.Tags{ {"d", "test-resource-id"}, {"name", "Test Resource"}, {"description", "This is a test resource"}, } event := createTestEvent(tags) - + amb, err := NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.Equal(event.ID, amb.ID) assert.Equal("test-resource-id", amb.D) assert.Equal("Test Resource", amb.Name) assert.Equal("This is a test resource", amb.Description) - + assert.Equal(event.ID, amb.EventID) assert.Equal(event.PubKey, amb.EventPubKey) assert.Equal(event.Content, amb.EventContent) @@ -82,39 +80,45 @@ func TestNostrToAMB_BasicFields(t *testing.T) { func TestNostrToAMB_About(t *testing.T) { assert := assert.New(t) - + tags := nostr.Tags{ {"d", "test-resource-id"}, - {"about", "http://w3id.org/kim/schulfaecher/s1009", "Französisch", "de"}, + {"about:id", "http://w3id.org/kim/schulfaecher/s1009"}, + {"about:prefLabel", "Französisch"}, + {"about:inLanguage", "de"}, + {"about:type", "Concept"}, } event := createTestEvent(tags) - + amb, err := NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.NotEmpty(amb.About) assert.Equal(1, len(amb.About)) assert.Equal("http://w3id.org/kim/schulfaecher/s1009", amb.About[0].ID) assert.Equal("Französisch", amb.About[0].PrefLabel) assert.Equal("de", amb.About[0].InLanguage) + assert.Equal("Concept", amb.About[0].Type) } func TestNostrToAMB_Keywords(t *testing.T) { assert := assert.New(t) - + tags := nostr.Tags{ {"d", "test-resource-id"}, - {"keywords", "Französisch", "Niveau A2", "Sprache"}, + {"t", "Französisch"}, + {"t", "Niveau A2"}, + {"t", "Sprache"}, } event := createTestEvent(tags) - + amb, err := NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.Equal(3, len(amb.Keywords)) assert.Contains(amb.Keywords, "Französisch") assert.Contains(amb.Keywords, "Niveau A2") @@ -123,19 +127,19 @@ func TestNostrToAMB_Keywords(t *testing.T) { func TestNostrToAMB_InLanguage(t *testing.T) { assert := assert.New(t) - + tags := nostr.Tags{ {"d", "test-resource-id"}, {"inLanguage", "fr"}, {"inLanguage", "de"}, } event := createTestEvent(tags) - + amb, err := NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.Equal(2, len(amb.InLanguage)) assert.Contains(amb.InLanguage, "fr") assert.Contains(amb.InLanguage, "de") @@ -143,43 +147,47 @@ func TestNostrToAMB_InLanguage(t *testing.T) { func TestNostrToAMB_Image(t *testing.T) { assert := assert.New(t) - + imageURL := "https://www.tutory.de/worksheet/fbbadf1a-145a-463d-9a43-1ae9965c86b9.jpg?width=1000" tags := nostr.Tags{ {"d", "test-resource-id"}, {"image", imageURL}, } event := createTestEvent(tags) - + amb, err := NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.Equal(imageURL, amb.Image) } func TestNostrToAMB_Creator(t *testing.T) { assert := assert.New(t) - + tags := nostr.Tags{ {"d", "test-resource-id"}, - {"creator", "http://author1.org", "Autorin 1", "Person"}, - {"creator", "http://author2.org", "Autorin 2", "Person"}, + {"creator:id", "http://author1.org"}, + {"creator:name", "Autorin 1"}, + {"creator:type", "Person"}, + {"creator:id", "http://author2.org"}, + {"creator:name", "Autorin 2"}, + {"creator:type", "Person"}, } event := createTestEvent(tags) - + amb, err := NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.Equal(2, len(amb.Creator)) - + assert.Equal("http://author1.org", amb.Creator[0].ID) assert.Equal("Autorin 1", amb.Creator[0].Name) assert.Equal("Person", amb.Creator[0].Type) - + assert.Equal("http://author2.org", amb.Creator[1].ID) assert.Equal("Autorin 2", amb.Creator[1].Name) assert.Equal("Person", amb.Creator[1].Type) @@ -187,25 +195,29 @@ func TestNostrToAMB_Creator(t *testing.T) { func TestNostrToAMB_Contributor(t *testing.T) { assert := assert.New(t) - + tags := nostr.Tags{ {"d", "test-resource-id"}, - {"contributor", "http://author1.org", "Autorin 1", "Person"}, - {"contributor", "http://author2.org", "Autorin 2", "Person"}, + {"contributor:id", "http://author1.org"}, + {"contributor:name", "Autorin 1"}, + {"contributor:type", "Person"}, + {"contributor:id", "http://author2.org"}, + {"contributor:name", "Autorin 2"}, + {"contributor:type", "Person"}, } event := createTestEvent(tags) - + amb, err := NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.Equal(2, len(amb.Contributor)) - + assert.Equal("http://author1.org", amb.Contributor[0].ID) assert.Equal("Autorin 1", amb.Contributor[0].Name) assert.Equal("Person", amb.Contributor[0].Type) - + assert.Equal("http://author2.org", amb.Contributor[1].ID) assert.Equal("Autorin 2", amb.Contributor[1].Name) assert.Equal("Person", amb.Contributor[1].Type) @@ -213,7 +225,7 @@ func TestNostrToAMB_Contributor(t *testing.T) { func TestNostrToAMB_Dates(t *testing.T) { assert := assert.New(t) - + tags := nostr.Tags{ {"d", "test-resource-id"}, {"dateCreated", "2019-07-02"}, @@ -221,12 +233,12 @@ func TestNostrToAMB_Dates(t *testing.T) { {"dateModified", "2019-07-04"}, } event := createTestEvent(tags) - + amb, err := NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.Equal("2019-07-02", amb.DateCreated) assert.Equal("2019-07-03", amb.DatePublished) assert.Equal("2019-07-04", amb.DateModified) @@ -234,25 +246,29 @@ func TestNostrToAMB_Dates(t *testing.T) { func TestNostrToAMB_Publisher(t *testing.T) { assert := assert.New(t) - + tags := nostr.Tags{ {"d", "test-resource-id"}, - {"publisher", "http://publisher1.org", "Publisher 1", "Person"}, - {"publisher", "http://publisher2.org", "Publisher 2", "Organization"}, + {"publisher:id", "http://publisher1.org"}, + {"publisher:name", "Publisher 1"}, + {"publisher:type", "Person"}, + {"publisher:id", "http://publisher2.org"}, + {"publisher:name", "Publisher 2"}, + {"publisher:type", "Organization"}, } event := createTestEvent(tags) - + amb, err := NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.Equal(2, len(amb.Publisher)) - + assert.Equal("http://publisher1.org", amb.Publisher[0].ID) assert.Equal("Publisher 1", amb.Publisher[0].Name) assert.Equal("Person", amb.Publisher[0].Type) - + assert.Equal("http://publisher2.org", amb.Publisher[1].ID) assert.Equal("Publisher 2", amb.Publisher[1].Name) assert.Equal("Organization", amb.Publisher[1].Type) @@ -260,25 +276,29 @@ func TestNostrToAMB_Publisher(t *testing.T) { func TestNostrToAMB_Funder(t *testing.T) { assert := assert.New(t) - + tags := nostr.Tags{ {"d", "test-resource-id"}, - {"funder", "http://funder1.org", "Funder 1", "Person"}, - {"funder", "http://funder2.org", "Funder 2", "Organization"}, + {"funder:id", "http://funder1.org"}, + {"funder:name", "Funder 1"}, + {"funder:type", "Person"}, + {"funder:id", "http://funder2.org"}, + {"funder:name", "Funder 2"}, + {"funder:type", "Organization"}, } event := createTestEvent(tags) - + amb, err := NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.Equal(2, len(amb.Funder)) - + assert.Equal("http://funder1.org", amb.Funder[0].ID) assert.Equal("Funder 1", amb.Funder[0].Name) assert.Equal("Person", amb.Funder[0].Type) - + assert.Equal("http://funder2.org", amb.Funder[1].ID) assert.Equal("Funder 2", amb.Funder[1].Name) assert.Equal("Organization", amb.Funder[1].Type) @@ -286,49 +306,50 @@ func TestNostrToAMB_Funder(t *testing.T) { func TestNostrToAMB_IsAccessibleForFree(t *testing.T) { assert := assert.New(t) - + tags := nostr.Tags{ {"d", "test-resource-id"}, {"isAccessibleForFree", "true"}, } event := createTestEvent(tags) - + amb, err := NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.True(amb.IsAccessibleForFree) - + // Test with "false" value tags = nostr.Tags{ {"d", "test-resource-id"}, {"isAccessibleForFree", "false"}, } event = createTestEvent(tags) - + amb, err = NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.False(amb.IsAccessibleForFree) } func TestNostrToAMB_License(t *testing.T) { assert := assert.New(t) - + tags := nostr.Tags{ {"d", "test-resource-id"}, - {"license", "https://creativecommons.org/publicdomain/zero/1.0/", "CC-0"}, + {"license:id", "https://creativecommons.org/publicdomain/zero/1.0/"}, + {"license:name", "CC-0"}, } event := createTestEvent(tags) - + amb, err := NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.NotNil(amb.License) assert.Equal("https://creativecommons.org/publicdomain/zero/1.0/", amb.License.ID) assert.Equal("CC-0", amb.License.Name) @@ -336,18 +357,21 @@ func TestNostrToAMB_License(t *testing.T) { func TestNostrToAMB_ConditionsOfAccess(t *testing.T) { assert := assert.New(t) - + tags := nostr.Tags{ {"d", "test-resource-id"}, - {"conditionsOfAccess", "http://w3id.org/kim/conditionsOfAccess/no_login", "Kein Login", "de"}, + {"conditionsOfAccess:id", "http://w3id.org/kim/conditionsOfAccess/no_login"}, + {"conditionsOfAccess:prefLabel", "Kein Login"}, + {"conditionsOfAccess:inLanguage", "de"}, + {"conditionsOfAccess:type", "Concept"}, } event := createTestEvent(tags) - + amb, err := NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.NotNil(amb.ConditionsOfAccess) assert.Equal("http://w3id.org/kim/conditionsOfAccess/no_login", amb.ConditionsOfAccess.ID) assert.Equal("Kein Login", amb.ConditionsOfAccess.PrefLabel) @@ -356,25 +380,29 @@ func TestNostrToAMB_ConditionsOfAccess(t *testing.T) { func TestNostrToAMB_LearningResourceType(t *testing.T) { assert := assert.New(t) - + tags := nostr.Tags{ {"d", "test-resource-id"}, - {"learningResourceType", "http://w3id.org/openeduhub/vocabs/new_lrt/video", "Video", "de"}, - {"learningResourceType", "http://w3id.org/openeduhub/vocabs/new_lrt/tutorial", "Tutorial", "en"}, + {"learningResourceType:id", "http://w3id.org/openeduhub/vocabs/new_lrt/video"}, + {"learningResourceType:prefLabel", "Video"}, + {"learningResourceType:inLanguage", "de"}, + {"learningResourceType:id", "http://w3id.org/openeduhub/vocabs/new_lrt/tutorial"}, + {"learningResourceType:prefLabel", "Tutorial"}, + {"learningResourceType:inLanguage", "en"}, } event := createTestEvent(tags) - + amb, err := NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.Equal(2, len(amb.LearningResourceType)) - + assert.Equal("http://w3id.org/openeduhub/vocabs/new_lrt/video", amb.LearningResourceType[0].ID) assert.Equal("Video", amb.LearningResourceType[0].PrefLabel) assert.Equal("de", amb.LearningResourceType[0].InLanguage) - + assert.Equal("http://w3id.org/openeduhub/vocabs/new_lrt/tutorial", amb.LearningResourceType[1].ID) assert.Equal("Tutorial", amb.LearningResourceType[1].PrefLabel) assert.Equal("en", amb.LearningResourceType[1].InLanguage) @@ -382,25 +410,29 @@ func TestNostrToAMB_LearningResourceType(t *testing.T) { func TestNostrToAMB_Audience(t *testing.T) { assert := assert.New(t) - + tags := nostr.Tags{ {"d", "test-resource-id"}, - {"audience", "http://purl.org/dcx/lrmi-vocabs/educationalAudienceRole/student", "Schüler:in", "de"}, - {"audience", "http://purl.org/dcx/lrmi-vocabs/educationalAudienceRole/teacher", "Lehrer:in", "de"}, + {"audience:id", "http://purl.org/dcx/lrmi-vocabs/educationalAudienceRole/student"}, + {"audience:prefLabel", "Schüler:in"}, + {"audience:inLanguage", "de"}, + {"audience:id", "http://purl.org/dcx/lrmi-vocabs/educationalAudienceRole/teacher"}, + {"audience:prefLabel", "Lehrer:in"}, + {"audience:inLanguage", "de"}, } event := createTestEvent(tags) - + amb, err := NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.Equal(2, len(amb.Audience)) - + assert.Equal("http://purl.org/dcx/lrmi-vocabs/educationalAudienceRole/student", amb.Audience[0].ID) assert.Equal("Schüler:in", amb.Audience[0].PrefLabel) assert.Equal("de", amb.Audience[0].InLanguage) - + assert.Equal("http://purl.org/dcx/lrmi-vocabs/educationalAudienceRole/teacher", amb.Audience[1].ID) assert.Equal("Lehrer:in", amb.Audience[1].PrefLabel) assert.Equal("de", amb.Audience[1].InLanguage) @@ -408,25 +440,29 @@ func TestNostrToAMB_Audience(t *testing.T) { func TestNostrToAMB_Teaches(t *testing.T) { assert := assert.New(t) - + tags := nostr.Tags{ {"d", "test-resource-id"}, - {"teaches", "http://awesome-skills.org/1", "Zuhören", "de"}, - {"teaches", "http://awesome-skills.org/2", "Sprechen", "de"}, + {"teaches:id", "http://awesome-skills.org/1"}, + {"teaches:prefLabel", "Zuhören"}, + {"teaches:inLanguage", "de"}, + {"teaches:id", "http://awesome-skills.org/2"}, + {"teaches:prefLabel", "Sprechen"}, + {"teaches:inLanguage", "de"}, } event := createTestEvent(tags) - + amb, err := NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.Equal(2, len(amb.Teaches)) - + assert.Equal("http://awesome-skills.org/1", amb.Teaches[0].ID) assert.Equal("Zuhören", amb.Teaches[0].PrefLabel) assert.Equal("de", amb.Teaches[0].InLanguage) - + assert.Equal("http://awesome-skills.org/2", amb.Teaches[1].ID) assert.Equal("Sprechen", amb.Teaches[1].PrefLabel) assert.Equal("de", amb.Teaches[1].InLanguage) @@ -434,25 +470,29 @@ func TestNostrToAMB_Teaches(t *testing.T) { func TestNostrToAMB_Assesses(t *testing.T) { assert := assert.New(t) - + tags := nostr.Tags{ {"d", "test-resource-id"}, - {"assesses", "http://awesome-skills.org/1", "Hörverständnis", "de"}, - {"assesses", "http://awesome-skills.org/2", "Grammatik", "de"}, + {"assesses:id", "http://awesome-skills.org/1"}, + {"assesses:prefLabel", "Hörverständnis"}, + {"assesses:inLanguage", "de"}, + {"assesses:id", "http://awesome-skills.org/2"}, + {"assesses:prefLabel", "Grammatik"}, + {"assesses:inLanguage", "de"}, } event := createTestEvent(tags) - + amb, err := NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.Equal(2, len(amb.Assesses)) - + assert.Equal("http://awesome-skills.org/1", amb.Assesses[0].ID) assert.Equal("Hörverständnis", amb.Assesses[0].PrefLabel) assert.Equal("de", amb.Assesses[0].InLanguage) - + assert.Equal("http://awesome-skills.org/2", amb.Assesses[1].ID) assert.Equal("Grammatik", amb.Assesses[1].PrefLabel) assert.Equal("de", amb.Assesses[1].InLanguage) @@ -460,25 +500,29 @@ func TestNostrToAMB_Assesses(t *testing.T) { func TestNostrToAMB_CompetencyRequired(t *testing.T) { assert := assert.New(t) - + tags := nostr.Tags{ {"d", "test-resource-id"}, - {"competencyRequired", "http://awesome-skills.org/1", "Basisvokabular", "de"}, - {"competencyRequired", "http://awesome-skills.org/2", "Grundkenntnisse", "de"}, + {"competencyRequired:id", "http://awesome-skills.org/1"}, + {"competencyRequired:prefLabel", "Basisvokabular"}, + {"competencyRequired:inLanguage", "de"}, + {"competencyRequired:id", "http://awesome-skills.org/2"}, + {"competencyRequired:prefLabel", "Grundkenntnisse"}, + {"competencyRequired:inLanguage", "de"}, } event := createTestEvent(tags) - + amb, err := NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.Equal(2, len(amb.CompetencyRequired)) - + assert.Equal("http://awesome-skills.org/1", amb.CompetencyRequired[0].ID) assert.Equal("Basisvokabular", amb.CompetencyRequired[0].PrefLabel) assert.Equal("de", amb.CompetencyRequired[0].InLanguage) - + assert.Equal("http://awesome-skills.org/2", amb.CompetencyRequired[1].ID) assert.Equal("Grundkenntnisse", amb.CompetencyRequired[1].PrefLabel) assert.Equal("de", amb.CompetencyRequired[1].InLanguage) @@ -486,25 +530,29 @@ func TestNostrToAMB_CompetencyRequired(t *testing.T) { func TestNostrToAMB_EducationalLevel(t *testing.T) { assert := assert.New(t) - + tags := nostr.Tags{ {"d", "test-resource-id"}, - {"educationalLevel", "https://w3id.org/kim/educationalLevel/level_2", "Sekundarstufe 1", "de"}, - {"educationalLevel", "https://w3id.org/kim/educationalLevel/level_3", "Sekundarstufe 2", "de"}, + {"educationalLevel:id", "https://w3id.org/kim/educationalLevel/level_2"}, + {"educationalLevel:prefLabel", "Sekundarstufe 1"}, + {"educationalLevel:inLanguage", "de"}, + {"educationalLevel:id", "https://w3id.org/kim/educationalLevel/level_3"}, + {"educationalLevel:prefLabel", "Sekundarstufe 2"}, + {"educationalLevel:inLanguage", "de"}, } event := createTestEvent(tags) - + amb, err := NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.Equal(2, len(amb.EducationalLevel)) - + assert.Equal("https://w3id.org/kim/educationalLevel/level_2", amb.EducationalLevel[0].ID) assert.Equal("Sekundarstufe 1", amb.EducationalLevel[0].PrefLabel) assert.Equal("de", amb.EducationalLevel[0].InLanguage) - + assert.Equal("https://w3id.org/kim/educationalLevel/level_3", amb.EducationalLevel[1].ID) assert.Equal("Sekundarstufe 2", amb.EducationalLevel[1].PrefLabel) assert.Equal("de", amb.EducationalLevel[1].InLanguage) @@ -512,18 +560,20 @@ func TestNostrToAMB_EducationalLevel(t *testing.T) { func TestNostrToAMB_InteractivityType(t *testing.T) { assert := assert.New(t) - + tags := nostr.Tags{ {"d", "test-resource-id"}, - {"interactivityType", "http://purl.org/dcx/lrmi-vocabs/interactivityType/active", "aktiv", "de"}, + {"interactivityType:id", "http://purl.org/dcx/lrmi-vocabs/interactivityType/active"}, + {"interactivityType:prefLabel", "aktiv"}, + {"interactivityType:inLanguage", "de"}, } event := createTestEvent(tags) - + amb, err := NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.NotNil(amb.InteractivityType) assert.Equal("http://purl.org/dcx/lrmi-vocabs/interactivityType/active", amb.InteractivityType.ID) assert.Equal("aktiv", amb.InteractivityType.PrefLabel) @@ -532,18 +582,19 @@ func TestNostrToAMB_InteractivityType(t *testing.T) { func TestNostrToAMB_IsBasedOn(t *testing.T) { assert := assert.New(t) - + tags := nostr.Tags{ {"d", "test-resource-id"}, - {"isBasedOn", "http://an-awesome-resource.org", "Französisch I"}, + {"isBasedOn:id", "http://an-awesome-resource.org"}, + {"isBasedOn:name", "Französisch I"}, } event := createTestEvent(tags) - + amb, err := NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.Equal(1, len(amb.IsBasedOn)) assert.Equal("http://an-awesome-resource.org", amb.IsBasedOn[0].ID) assert.Equal("Französisch I", amb.IsBasedOn[0].Name) @@ -551,18 +602,20 @@ func TestNostrToAMB_IsBasedOn(t *testing.T) { func TestNostrToAMB_IsPartOf(t *testing.T) { assert := assert.New(t) - + tags := nostr.Tags{ {"d", "test-resource-id"}, - {"isPartOf", "http://whole.org", "Whole", "PresentationDigitalDocument"}, + {"isPartOf:id", "http://whole.org"}, + {"isPartOf:name", "Whole"}, + {"isPartOf:type", "PresentationDigitalDocument"}, } event := createTestEvent(tags) - + amb, err := NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.Equal(1, len(amb.IsPartOf)) assert.Equal("http://whole.org", amb.IsPartOf[0].ID) assert.Equal("Whole", amb.IsPartOf[0].Name) @@ -571,24 +624,28 @@ func TestNostrToAMB_IsPartOf(t *testing.T) { func TestNostrToAMB_HasPart(t *testing.T) { assert := assert.New(t) - + tags := nostr.Tags{ {"d", "test-resource-id"}, - {"hasPart", "http://part1.org", "Part 1", "LearningResource"}, - {"hasPart", "http://part2.org", "Part 2", "LearningResource"}, + {"hasPart:id", "http://part1.org"}, + {"hasPart:name", "Part 1"}, + {"hasPart:type", "LearningResource"}, + {"hasPart:id", "http://part2.org"}, + {"hasPart:name", "Part 2"}, + {"hasPart:type", "LearningResource"}, } event := createTestEvent(tags) - + amb, err := NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.Equal(2, len(amb.HasPart)) assert.Equal("http://part1.org", amb.HasPart[0].ID) assert.Equal("Part 1", amb.HasPart[0].Name) assert.Equal("LearningResource", amb.HasPart[0].Type) - + assert.Equal("http://part2.org", amb.HasPart[1].ID) assert.Equal("Part 2", amb.HasPart[1].Name) assert.Equal("LearningResource", amb.HasPart[1].Type) @@ -596,42 +653,47 @@ func TestNostrToAMB_HasPart(t *testing.T) { func TestNostrToAMB_Duration(t *testing.T) { assert := assert.New(t) - + tags := nostr.Tags{ {"d", "test-resource-id"}, {"duration", "PT30M"}, } event := createTestEvent(tags) - + amb, err := NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.Equal("PT30M", amb.Duration) } func TestNostrToAMB_Trailer(t *testing.T) { assert := assert.New(t) - + tags := nostr.Tags{ {"d", "test-resource-id"}, - {"trailer", "https://example.com/video.mp4", "Video", "video/mp4", "10MB", "abc123", "https://example.com/embed", "1Mbps"}, + {"trailer:contentUrl", "https://example.com/video.mp4"}, + {"trailer:type", "VideoObject"}, + {"trailer:encodingFormat", "video/mp4"}, + {"trailer:contentSize", "10MB"}, + {"trailer:sha256", "abc123"}, + {"trailer:embedUrl", "https://example.com/embed"}, + {"trailer:bitrate", "1Mbps"}, } event := createTestEvent(tags) - + amb, err := NostrToAMB(event) - + assert.NoError(err) assert.NotNil(amb) - + assert.Equal(1, len(amb.Trailer)) assert.Equal("https://example.com/video.mp4", amb.Trailer[0].ContentUrl) - assert.Equal("Video", amb.Trailer[0].Type) + assert.Equal("VideoObject", amb.Trailer[0].Type) assert.Equal("video/mp4", amb.Trailer[0].EncodingFormat) assert.Equal("10MB", amb.Trailer[0].ContentSize) assert.Equal("abc123", amb.Trailer[0].Sha256) assert.Equal("https://example.com/embed", amb.Trailer[0].EmbedUrl) assert.Equal("1Mbps", amb.Trailer[0].Bitrate) } - diff --git a/typesense30142/types.go b/typesense30142/types.go index 194673f..c2c02d5 100644 --- a/typesense30142/types.go +++ b/typesense30142/types.go @@ -77,17 +77,17 @@ type Audience struct { // Teaches represents what the content teaches type Teaches struct { - ControlledVocabulary + ControlledVocabulary } // Assesses represents what the content assesses type Assesses struct { - ControlledVocabulary + ControlledVocabulary } // CompetencyRequired represents required competencies type CompetencyRequired struct { - ControlledVocabulary + ControlledVocabulary } // EducationalLevel represents the educational level @@ -102,7 +102,7 @@ type InteractivityType struct { // IsBasedOn represents a reference to source material type IsBasedOn struct { - ID string `json:"id"` + ID string `json:"id"` Type string `json:"type,omitempty"` Name string `json:"name"` Creator *Creator `json:"creator,omitempty"` @@ -135,6 +135,41 @@ type License struct { Name string `json:"name,omitempty"` } +// MainEntityProvider represents the provider of a web page +type MainEntityProvider struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` +} + +// MainEntityOfPage represents meta-metadata about web pages +type MainEntityOfPage struct { + ID string `json:"id"` + Type string `json:"type"` + Provider *MainEntityProvider `json:"provider,omitempty"` + DateCreated string `json:"dateCreated,omitempty"` + DateModified string `json:"dateModified,omitempty"` +} + +// Encoding represents a media encoding (MediaObject) +type Encoding struct { + Type string `json:"type"` + ContentUrl string `json:"contentUrl,omitempty"` + EmbedUrl string `json:"embedUrl,omitempty"` + EncodingFormat string `json:"encodingFormat,omitempty"` + ContentSize string `json:"contentSize,omitempty"` + Sha256 string `json:"sha256,omitempty"` + Bitrate string `json:"bitrate,omitempty"` +} + +// Caption represents a caption/subtitle file (MediaObject) +type Caption struct { + ID string `json:"id"` + Type string `json:"type"` + EncodingFormat string `json:"encodingFormat,omitempty"` + InLanguage string `json:"inLanguage,omitempty"` +} + // NostrMetadata contains Nostr-specific metadata type NostrMetadata struct { EventID string `json:"eventID"` @@ -190,9 +225,12 @@ type AMBMetadata struct { HasPart []*HasPart `json:"hasPart,omitempty"` // Technical - Duration string `json:"duration,omitempty"` - // TODO Encoding `` - // TODO Caption + Duration string `json:"duration,omitempty"` + Encoding []*Encoding `json:"encoding,omitempty"` + Caption []*Caption `json:"caption,omitempty"` + + // Meta-Metadata + MainEntityOfPage []*MainEntityOfPage `json:"mainEntityOfPage,omitempty"` // Nostr integration NostrMetadata `json:",inline"`