Skip to content

Commit 56d664f

Browse files
committed
now supports geocoordinates from csv
1 parent 204f395 commit 56d664f

5 files changed

Lines changed: 121 additions & 101 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# gocroissant
22

3-
[![Version](https://img.shields.io/badge/version-v0.2.4-blue)](https://github.com/beyondcivic/gocroissant/releases/tag/v0.2.4)
3+
[![Version](https://img.shields.io/badge/version-v0.2.5-blue)](https://github.com/beyondcivic/gocroissant/releases/tag/v0.2.5)
44
[![Go Version](https://img.shields.io/badge/Go-1.24+-00ADD8?logo=go)](https://golang.org/doc/devel/release.html)
55
[![License](https://img.shields.io/badge/license-TBD-red)](LICENSE)
66

pkg/croissant/croissant.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,16 +71,17 @@ func InferDataType(value string) string {
7171
func IsValidDataType(dataType string) bool {
7272
validTypes := map[string]bool{
7373
// Schema.org types
74-
"sc:Text": true,
75-
"sc:Boolean": true,
76-
"sc:Integer": true,
77-
"sc:Number": true,
78-
"sc:DateTime": true,
79-
"sc:URL": true,
80-
"sc:ImageObject": true,
81-
"sc:VideoObject": true,
82-
"sc:Enumeration": true,
83-
"sc:GeoShape": true,
74+
"sc:Text": true,
75+
"sc:Boolean": true,
76+
"sc:Integer": true,
77+
"sc:Number": true,
78+
"sc:DateTime": true,
79+
"sc:URL": true,
80+
"sc:ImageObject": true,
81+
"sc:VideoObject": true,
82+
"sc:Enumeration": true,
83+
"sc:GeoShape": true,
84+
"sc:GeoCoordinates": true,
8485

8586
// Croissant-specific types
8687
"cr:Label": true,

pkg/croissant/metadata_node.go

Lines changed: 52 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -102,59 +102,23 @@ func FromMetadata(metadata Metadata) *MetadataNode {
102102
},
103103
Type: rs.Type,
104104
Description: rs.Description,
105-
DataType: *rs.DataType,
106105
Key: rs.Key,
107106
Data: rs.Data,
108107
}
108+
109+
// Handle DataType safely - check for nil
110+
if rs.DataType != nil {
111+
rsNode.DataType = *rs.DataType
112+
} else {
113+
// Set empty DataType if nil
114+
rsNode.DataType = DataType{}
115+
}
116+
109117
rsNode.SetParent(node)
110118

111119
// Convert fields
112120
for _, field := range rs.Fields {
113-
fieldNode := &FieldNode{
114-
BaseNode: BaseNode{
115-
ID: field.ID,
116-
Name: field.Name,
117-
},
118-
Type: field.Type,
119-
Description: field.Description,
120-
DataType: field.DataType,
121-
Source: SourceNode{
122-
Extract: ExtractNode{
123-
Column: field.Source.Extract.Column,
124-
JSONPath: field.Source.Extract.JSONPath,
125-
FileProperty: field.Source.Extract.FileProperty,
126-
},
127-
FileObject: FileObjectRef{
128-
ID: field.Source.FileObject.ID,
129-
},
130-
FileSet: FileObjectRef{
131-
ID: field.Source.FileSet.ID,
132-
},
133-
Transform: field.Source.Transform,
134-
Format: field.Source.Format,
135-
},
136-
Repeated: field.Repeated,
137-
Examples: field.Examples,
138-
References: field.References,
139-
}
140-
fieldNode.SetParent(rsNode)
141-
142-
// Convert subfields if they exist
143-
for _, subField := range field.SubField {
144-
subFieldNode := &FieldNode{
145-
BaseNode: BaseNode{
146-
ID: subField.ID,
147-
Name: subField.Name,
148-
},
149-
Type: subField.Type,
150-
Description: subField.Description,
151-
DataType: subField.DataType,
152-
// Note: subfields may have their own sources, but for now we'll handle basic conversion
153-
}
154-
subFieldNode.SetParent(fieldNode)
155-
fieldNode.SubField = append(fieldNode.SubField, subFieldNode)
156-
}
157-
121+
fieldNode := convertFieldToNode(field, rsNode)
158122
rsNode.Fields = append(rsNode.Fields, fieldNode)
159123
}
160124

@@ -164,6 +128,47 @@ func FromMetadata(metadata Metadata) *MetadataNode {
164128
return node
165129
}
166130

131+
// convertFieldToNode converts a Field to a FieldNode with proper nil handling
132+
func convertFieldToNode(field Field, parent Node) *FieldNode {
133+
fieldNode := &FieldNode{
134+
BaseNode: BaseNode{
135+
ID: field.ID,
136+
Name: field.Name,
137+
},
138+
Type: field.Type,
139+
Description: field.Description,
140+
DataType: field.DataType,
141+
Source: SourceNode{
142+
Extract: ExtractNode{
143+
Column: field.Source.Extract.Column,
144+
JSONPath: field.Source.Extract.JSONPath,
145+
Regex: field.Source.Extract.Regex,
146+
FileProperty: field.Source.Extract.FileProperty,
147+
},
148+
FileObject: FileObjectRef{
149+
ID: field.Source.FileObject.ID,
150+
},
151+
FileSet: FileObjectRef{
152+
ID: field.Source.FileSet.ID,
153+
},
154+
Transform: field.Source.Transform,
155+
Format: field.Source.Format,
156+
},
157+
Repeated: field.Repeated,
158+
Examples: field.Examples,
159+
References: field.References,
160+
}
161+
fieldNode.SetParent(parent)
162+
163+
// Convert subfields if they exist
164+
for _, subField := range field.SubField {
165+
subFieldNode := convertFieldToNode(subField, fieldNode)
166+
fieldNode.SubField = append(fieldNode.SubField, subFieldNode)
167+
}
168+
169+
return fieldNode
170+
}
171+
167172
// DistributionNode represents a file distribution
168173
type DistributionNode struct {
169174
BaseNode
@@ -410,6 +415,7 @@ func (s *SourceNode) ValidateSource() bool {
410415

411416
// ExtractNode represents extraction details
412417
type ExtractNode struct {
418+
Regex string `json:"regex,omitempty"`
413419
Column string `json:"column,omitempty"`
414420
JSONPath string `json:"jsonPath,omitempty"`
415421
FileProperty string `json:"fileProperty,omitempty"`

pkg/croissant/structs.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,3 +333,13 @@ func NewArrayDataType(dataTypes ...string) DataType {
333333
ArrayType: dataTypes,
334334
}
335335
}
336+
337+
// ValidateSource validates the source configuration
338+
func (fs FieldSource) ValidateSource() bool {
339+
// If no source is configured, it's invalid unless it's a parent field with subfields
340+
hasFileObject := fs.FileObject.ID != "" || fs.FileSet.ID != ""
341+
hasExtract := fs.Extract.Column != "" || fs.Extract.JSONPath != "" || fs.Extract.FileProperty != "" || fs.Extract.Regex != ""
342+
343+
// A valid source needs either a file object reference with extraction info, or other valid configurations
344+
return hasFileObject && (hasExtract || fs.Format != "")
345+
}

pkg/croissant/validation.go

Lines changed: 47 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ func validateRecordSetKey(rs *RecordSetNode, issues *Issues) {
249249

250250
// ValidateFieldNode validates a field node
251251
func ValidateFieldNode(field *FieldNode, issues *Issues, options ValidationOptions) {
252+
if field == nil {
253+
return
254+
}
255+
252256
if field.Name == "" {
253257
issues.AddError("Property \"https://schema.org/name\" is mandatory, but does not exist.", field)
254258
}
@@ -257,29 +261,64 @@ func ValidateFieldNode(field *FieldNode, issues *Issues, options ValidationOptio
257261
issues.AddError(fmt.Sprintf("\"%s\" should have an attribute \"@type\": \"http://mlcommons.org/croissant/Field\". Got %s instead.", field.Name, field.Type), field)
258262
}
259263

264+
// More explicit dataType validation
260265
if field.DataType.GetFirstType() == "" {
261-
issues.AddError("The field does not specify a valid http://mlcommons.org/croissant/dataType, neither does any of its predecessor.", field)
266+
issues.AddError(fmt.Sprintf("Field \"%s\" is missing required \"dataType\" property.", field.Name), field)
262267
} else if options.CheckDataTypes {
263-
allValid, invalidTypes := validateDataTypes(field.DataType)
264-
if !allValid {
268+
if isValid, invalidTypes := validateDataTypes(field.DataType); !isValid {
265269
for _, invalidType := range invalidTypes {
266-
issues.AddWarning(fmt.Sprintf("DataType \"%s\" is not a recognized schema.org type.", invalidType), field)
270+
issues.AddError(fmt.Sprintf("Field \"%s\" has invalid dataType \"%s\". Valid types include: sc:Text, sc:Number, sc:Boolean, sc:DateTime, sc:URL, sc:GeoCoordinates, sc:ImageObject, cr:BoundingBox, etc.", field.Name, invalidType), field)
267271
}
268272
}
269273
}
270274

271275
// Strict mode validations for fields
272276
if options.StrictMode {
273277
if field.Description == "" {
274-
issues.AddWarning(fmt.Sprintf("Field \"%s\" is missing a description.", field.Name), field)
278+
issues.AddWarning(fmt.Sprintf("Field \"%s\" is missing recommended \"description\" property.", field.Name), field)
279+
}
280+
}
281+
282+
// Check if field has subfields - use len check that's safe even if SubFields is nil
283+
hasSubFields := field.SubField != nil && len(field.SubField) > 0
284+
285+
// Only validate source for leaf fields (fields without subfields)
286+
if !hasSubFields {
287+
// Check if source is properly configured
288+
if !hasValidFieldSource(field) {
289+
issues.AddError(fmt.Sprintf("Field \"%s\" has invalid or missing source configuration.", field.Name), field)
275290
}
276291
}
277292

278-
if !field.Source.ValidateSource() {
279-
issues.AddError(fmt.Sprintf("Node \"%s\" is a field and has no source. Please, use http://mlcommons.org/croissant/source to specify the source.", field.ID), field)
293+
// Validate subfields recursively
294+
if field.SubField != nil {
295+
for _, subField := range field.SubField {
296+
if subField != nil {
297+
subField.SetParent(field)
298+
ValidateFieldNode(subField, issues, options)
299+
}
300+
}
280301
}
281302
}
282303

304+
// hasValidFieldSource checks if a field node has valid source configuration
305+
func hasValidFieldSource(field *FieldNode) bool {
306+
if field == nil {
307+
return false
308+
}
309+
310+
// Check if the source has file object reference and extraction method
311+
hasFileObject := field.Source.FileObject.ID != ""
312+
hasExtract := field.Source.Extract.Column != "" ||
313+
field.Source.Extract.JSONPath != "" ||
314+
field.Source.Extract.FileProperty != "" ||
315+
field.Source.Extract.Regex != ""
316+
hasFormat := field.Source.Format != ""
317+
318+
// A valid source needs either a file object reference with extraction info, or format
319+
return hasFileObject && (hasExtract || hasFormat)
320+
}
321+
283322
// ValidateCrossReferences validates that all references are valid
284323
func ValidateCrossReferences(node *MetadataNode, issues *Issues) {
285324
// Build a map of all available IDs
@@ -443,42 +482,6 @@ func isValidEncodingFormat(format string) bool {
443482
return validFormats[format] || strings.HasPrefix(format, "text/") || strings.HasPrefix(format, "application/") || strings.HasPrefix(format, "image/") || strings.HasPrefix(format, "audio/") || strings.HasPrefix(format, "video/")
444483
}
445484

446-
func isValidDataType(dataType string) bool {
447-
validTypes := map[string]bool{
448-
"sc:Text": true,
449-
"sc:Integer": true,
450-
"sc:Number": true,
451-
"sc:Float": true,
452-
"sc:Boolean": true,
453-
"sc:Date": true,
454-
"sc:DateTime": true,
455-
"sc:Time": true,
456-
"sc:URL": true,
457-
"sc:ImageObject": true,
458-
"sc:name": true, // Added for categorical data enumerations
459-
"sc:Enumeration": true, // Added for enumeration RecordSet dataType
460-
"cr:BoundingBox": true,
461-
// Wikidata entities are also valid (they start with "wd:")
462-
}
463-
464-
// Check if it's a known schema.org or croissant type
465-
if validTypes[dataType] {
466-
return true
467-
}
468-
469-
// Allow Wikidata entity IDs (format: wd:Q followed by numbers)
470-
if len(dataType) > 3 && dataType[:3] == "wd:" {
471-
return true
472-
}
473-
474-
// Allow full URLs (https://schema.org/... or https://www.wikidata.org/...)
475-
if len(dataType) > 8 && (dataType[:8] == "https://" || dataType[:7] == "http://") {
476-
return true
477-
}
478-
479-
return false
480-
}
481-
482485
// validateDataTypes validates all data types in a DataType (single or array)
483486
func validateDataTypes(dt DataType) (bool, []string) {
484487
types := dt.GetTypes()
@@ -490,7 +493,7 @@ func validateDataTypes(dt DataType) (bool, []string) {
490493
allValid := true
491494

492495
for _, dataType := range types {
493-
if !isValidDataType(dataType) {
496+
if !IsValidDataType(dataType) {
494497
allValid = false
495498
invalidTypes = append(invalidTypes, dataType)
496499
}

0 commit comments

Comments
 (0)