queryfy

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 14, 2025 License: Apache-2.0 Imports: 4 Imported by: 2

README

Queryfy

Validate and Query dynamic data in Go

Queryfy is a Go package for working with map-based data structures. It provides schema validation and querying capabilities for scenarios involving dynamic data like JSON APIs and configuration files.

Features

  • Schema validation with strict or loose modes
  • Query nested data using dot notation and array indexing
  • Type safety with validation-time type checking
  • Clear error messages with exact field paths
  • Composable schemas with AND/OR/NOT logic
  • Fluent builder API for intuitive schema definitions
  • Data transformation with built-in and custom transformers
  • DateTime validation with comprehensive format support
  • Dependent field validation for conditional requirements

Why Queryfy?

Existing Go solutions address only parts of the dynamic data problem. Libraries like go-playground/validator excel at struct validation but don't handle map[string]interface{} well. gojsonschema provides JSON Schema validation but lacks querying capabilities and requires verbose schema definitions. tidwall/gjson offers excellent querying but no validation. Queryfy combines validation and querying in a single, cohesive package designed specifically for Go's map-based dynamic data.

Installation

go get github.com/ha1tch/queryfy

Quick Start

package main

import (
    "fmt"
    "log"
    
    qf "github.com/ha1tch/queryfy"
    "github.com/ha1tch/queryfy/builders"
)

func main() {
    // Define a schema
    schema := builders.Object().
        Field("customerId", builders.String().Required()).
        Field("amount", builders.Number().Min(0).Required()).
        Field("items", builders.Array().Of(
            builders.Object().
                Field("productId", builders.String().Required()).
                Field("quantity", builders.Number().Min(1)).
                Field("price", builders.Number().Min(0))
        ).MinItems(1))

    // Your data
    orderData := map[string]interface{}{
        "customerId": "CUST-123",
        "amount": 150.50,
        "items": []interface{}{
            map[string]interface{}{
                "productId": "PROD-456",
                "quantity":  2,
                "price":     75.25,
            },
        },
    }

    // Validate
    if err := qf.Validate(orderData, schema); err != nil {
        log.Printf("Validation failed: %v\n", err)
        return
    }

    // Query
    firstPrice, _ := qf.Query(orderData, "items[0].price")
    fmt.Printf("First item price: $%.2f\n", firstPrice)
}

Core Concepts

Schema Definition

Define schemas using the fluent builder pattern:

userSchema := builders.Object().
    Field("id", builders.String().Pattern("^[A-Z]{3}-[0-9]{6}$")).
    Field("email", builders.String().Email().Required()).
    Field("age", builders.Number().Min(0).Max(150)).
    Field("roles", builders.Array().Of(builders.String().Enum("admin", "user", "guest"))).
    Field("address", builders.Object().
        Field("street", builders.String().Required()).
        Field("city", builders.String().Required()).
        Field("zipCode", builders.String().Pattern("^[0-9]{5}$"))
    )
Validation Modes

Strict Mode (Default): All fields must match the schema exactly. Extra fields cause validation errors.

err := qf.Validate(data, schema) // Uses strict mode by default

Loose Mode: Allows extra fields and validates type compatibility. For example, the string "42" is considered valid for a number field.

err := qf.ValidateWithMode(data, schema, qf.Loose)

Note: In v0.1.0, loose mode only validates type compatibility. It does not transform the actual data. The string "42" will validate as a number but remain a string in your data structure.

Querying Data

Query using simple path expressions:

// Simple field access
name, _ := qf.Query(data, "customer.firstName")

// Array access by index
firstItem, _ := qf.Query(data, "items[0]")

// Nested access
street, _ := qf.Query(data, "customer.address.street")

// Complex paths
price, _ := qf.Query(data, "items[0].product.price")
Composite Schemas (AND/OR/NOT)

Create complex validation logic by combining schemas:

// Email OR phone required
contactSchema := builders.Or(
    builders.String().Email(),
    builders.String().Pattern(`^\+?[1-9]\d{9,14}$`) // International phone
)

// Multiple conditions with AND
ageSchema := builders.And(
    builders.Number().Min(0),
    builders.Number().Max(150),
    builders.Number().Integer()
)

// NOT condition
nonEmptyString := builders.And(
    builders.String(),
    builders.Not(builders.String().Length(0))
)

// Use in object schema
schema := builders.Object().
    Field("contact", contactSchema.Required()).
    Field("age", ageSchema).
    Field("description", nonEmptyString)

Advanced Usage

Custom Validators

Create custom validation logic:

phoneValidator := builders.Custom(func(value interface{}) error {
    str, ok := value.(string)
    if !ok {
        return fmt.Errorf("expected string, got %T", value)
    }
    if !isValidPhone(str) {
        return fmt.Errorf("invalid phone number: %s", str)
    }
    return nil
})

schema := builders.Object().
    Field("phone", phoneValidator.Required())
Data Transformation

Transform data during validation using built-in or custom transformers:

import "github.com/ha1tch/queryfy/builders/transformers"

// Use built-in transformers
emailSchema := builders.Transform(
    builders.String().Email().Required(),
).Add(transformers.Trim()).
  Add(transformers.Lowercase())

// Number transformations
priceSchema := builders.Transform(
    builders.Number().Min(0).Required(),
).Add(transformers.RemoveCurrencySymbols()).
  Add(transformers.ToFloat64()).
  Add(transformers.Round(2))

// Custom transformer
normalizePhone := func(value interface{}) (interface{}, error) {
    phone := value.(string)
    // Remove all non-digits
    return regexp.MustCompile(`\D`).ReplaceAllString(phone, ""), nil
}

phoneSchema := builders.Transform(
    builders.String().Pattern(`^\d{10}$`).Required(),
).Add(normalizePhone)

// Using ValidateAndTransform to get transformed values
ctx := qf.NewValidationContext(qf.Strict)
transformed, err := emailSchema.ValidateAndTransform(emailInput, ctx)
DateTime Validation

Comprehensive date and time validation with multiple format support:

// Date only validation
birthDateSchema := builders.DateTime().
    DateOnly().              // YYYY-MM-DD format
    Past().                  // Must be in the past
    Age(18, 100).           // Age between 18 and 100
    Required()

// Timestamp validation
createdAtSchema := builders.DateTime().
    ISO8601().              // Full ISO8601 format
    Required()

// Custom format
appointmentSchema := builders.DateTime().
    Format("2006-01-02 15:04").
    Future().               // Must be in the future
    BusinessDay().          // Monday-Friday only
    Between(businessStart, businessEnd)
Dependent Field Validation

Validate fields based on the values of other fields:

// Payment form with conditional fields
paymentSchema := builders.Object().WithDependencies().
    Field("paymentMethod", builders.String().
        Enum("credit_card", "paypal", "bank_transfer")).
    DependentField("cardNumber",
        builders.Dependent("cardNumber").
            When(builders.WhenEquals("paymentMethod", "credit_card")).
            Then(builders.String().Pattern(`^\d{16}$`).Required())).
    DependentField("paypalEmail",
        builders.Dependent("paypalEmail").
            When(builders.WhenEquals("paymentMethod", "paypal")).
            Then(builders.String().Email().Required())).
    DependentField("accountNumber",
        builders.Dependent("accountNumber").
            When(builders.WhenEquals("paymentMethod", "bank_transfer")).
            Then(builders.String().Required()))
Schema Composition

Build reusable schema components:

// Reusable address schema
addressSchema := builders.Object().
    Field("street", builders.String().Required()).
    Field("city", builders.String().Required()).
    Field("zipCode", builders.String().Pattern("^[0-9]{5}$"))

// Use in multiple places
customerSchema := builders.Object().
    Field("name", builders.String().Required()).
    Field("billingAddress", addressSchema.Required()).
    Field("shippingAddress", addressSchema)
Pre-Marshal Validation

Ensure data is valid before JSON marshaling:

func processOrder(data map[string]interface{}) error {
    if err := qf.Validate(data, orderSchema); err != nil {
        return fmt.Errorf("invalid order data: %w", err)
    }
    
    bytes, _ := json.Marshal(data)
    return sendToAPI(bytes)
}

Real-World Example

// E-commerce order validation
orderSchema := builders.Object().
    Field("orderId", builders.String().Required()).
    Field("customer", builders.Object().
        Field("email", builders.Transform(
            builders.String().Email().Required(),
        ).Add(transformers.Trim()).Add(transformers.Lowercase())).
        Field("phone", builders.String().Optional())
    ).Required().
    Field("payment", builders.Object().
        Field("method", builders.String().Enum("CARD", "CASH", "DIGITAL_WALLET")).
        Field("amount", builders.Number().Min(0).Required()).
        Field("currency", builders.String().Length(3).Required())
    ).Required().
    Field("items", builders.Array().MinItems(1).Of(
        builders.Object().
            Field("productId", builders.String().Required()).
            Field("quantity", builders.Number().Min(1).Integer().Required()).
            Field("price", builders.Transform(
                builders.Number().Min(0).Required(),
            ).Add(transformers.Round(2)))
    ).Required())

// Validate incoming order
if err := qf.Validate(orderData, orderSchema); err != nil {
    validationErr := err.(*qf.ValidationError)
    for _, fieldErr := range validationErr.Errors {
        log.Printf("Field %s: %s", fieldErr.Path, fieldErr.Message)
    }
    return err
}

// Query order data
customerEmail, _ := qf.Query(orderData, "customer.email")
totalAmount, _ := qf.Query(orderData, "payment.amount")
firstProductId, _ := qf.Query(orderData, "items[0].productId")

Error Messages

Queryfy provides clear, actionable error messages with exact field paths:

validation failed:
  customer.email: must be a valid email address, got "not-an-email"
  items[0].quantity: must be >= 1, got 0
  items[2].productId: field is required
  payment.method: must be one of: CARD, CASH, DIGITAL_WALLET, got "CHECK"

Performance

Queryfy is designed for production use:

  • Validation schemas can be defined once and reused
  • Query paths are cached after first use
  • Minimal reflection through type-switch optimization
  • No external dependencies
// Create schema once
var orderSchema = builders.Object().
    Field("id", builders.String().Required()).
    Field("amount", builders.Number().Min(0))

// Reuse for multiple validations
for _, order := range orders {
    if err := qf.Validate(order, orderSchema); err != nil {
        // Handle error
    }
}

Roadmap

v0.1.0 (Released)
  • [✓] Schema validation with builder API
  • [✓] Basic path queries (dot notation, array indexing)
  • [✓] Composite schemas (AND/OR/NOT)
  • [✓] Strict and loose validation modes
  • [✓] Custom validators
  • [✓] Clear error messages with paths
v0.2.0 (Current Release)
  • [✓] Data transformation pipeline with builder pattern
  • [✓] DateTime validation with comprehensive format support
  • [✓] Dependent field validation for conditional requirements
  • [✓] Phone normalization for multiple countries
  • [✓] Built-in transformers (string, number, date operations)
  • [✓] Transform chaining with .Add() method
v0.3.0 (Planned)
  • Data transformation in loose mode (modify actual data)
  • Wildcard queries (items[*].price)
  • Schema compilation for better performance
  • Iteration methods (Each, Collect, ValidateEach)
  • Enhanced transform API (direct method chaining)
v0.4.0 (Future)
  • Filter expressions (items[?price > 100])
  • Aggregation functions (sum(), avg(), count())
  • JSON Schema compatibility
  • Struct conversion (ToStruct, ValidateToStruct)

License

Copyright 2025 h@ual.fi

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Documentation

Overview

Package queryfy provides schema validation and querying for dynamic data in Go.

Queryfy is designed to work with map[string]interface{} data structures commonly found in JSON APIs, configuration files, and other dynamic contexts.

Basic usage:

schema := queryfy.Object().
	Field("id", queryfy.String().Required()).
	Field("amount", queryfy.Number().Min(0))

data := map[string]interface{}{
	"id": "order-123",
	"amount": 99.99,
}

if err := queryfy.Validate(data, schema); err != nil {
	// Handle validation error
}

// Query the data
amount, _ := queryfy.Query(data, "amount")

types.go - Updated with DateTime, Dependent, and Transform types

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ConvertStringToNumber

func ConvertStringToNumber(str string) (float64, bool)

ConvertStringToNumber attempts to convert a string to a number. Returns the number and true if successful, 0 and false otherwise.

func ConvertToString

func ConvertToString(value interface{}) (string, bool)

ConvertToString attempts to convert a value to string. Returns the string and true if successful, empty string and false otherwise.

func MustValidate

func MustValidate(data interface{}, schema Schema)

MustValidate validates data against a schema and panics on error. This is useful in initialization code where validation errors are fatal.

func Query

func Query(data interface{}, queryStr string) (interface{}, error)

Query executes a query against the data and returns the result. Supports dot notation and array indexing:

  • "name" - returns the value of field "name"
  • "user.email" - returns nested field value
  • "items[0]" - returns first element of array
  • "items[0].price" - returns field from array element

func Validate

func Validate(data interface{}, schema Schema) error

Validate validates data against a schema. Returns a ValidationError containing all validation failures, or nil if valid.

func ValidateValue

func ValidateValue(value interface{}, expectedType SchemaType, ctx *ValidationContext) bool

ValidateValue is a helper function that validates a single value against its expected type. It handles type checking and conversion based on the validation mode.

func ValidateWithMode

func ValidateWithMode(data interface{}, schema Schema, mode ValidationMode) error

ValidateWithMode validates data against a schema with a specific validation mode.

func WrapError

func WrapError(err error, path string) error

WrapError wraps an error with field path information. If the error is already a ValidationError, it prepends the path to all field errors. Otherwise, it creates a new ValidationError with a single field error.

Types

type BaseSchema

type BaseSchema struct {
	SchemaType SchemaType // Changed from schemaType to SchemaType to make it accessible
	// contains filtered or unexported fields
}

BaseSchema provides common functionality for all schema types. It should be embedded in concrete schema implementations.

func (*BaseSchema) CheckRequired

func (s *BaseSchema) CheckRequired(value interface{}, ctx *ValidationContext) bool

CheckRequired checks if a required field is present and not nil. Returns true if validation should continue, false if it should stop.

func (*BaseSchema) IsNullable

func (s *BaseSchema) IsNullable() bool

IsNullable returns true if the field can be null.

func (*BaseSchema) IsRequired

func (s *BaseSchema) IsRequired() bool

IsRequired returns true if the field is required.

func (*BaseSchema) SetNullable

func (s *BaseSchema) SetNullable(nullable bool)

SetNullable sets whether the field can be null.

func (*BaseSchema) SetRequired

func (s *BaseSchema) SetRequired(required bool)

SetRequired sets whether the field is required.

func (*BaseSchema) Type

func (s *BaseSchema) Type() SchemaType

Type returns the schema type.

type FieldError

type FieldError struct {
	// Path is the field path where the error occurred (e.g., "user.email" or "items[0].price")
	Path string
	// Message describes what validation failed
	Message string
	// Value is the actual value that failed validation (optional)
	Value interface{}
}

FieldError represents a validation error for a specific field.

func NewFieldError

func NewFieldError(path, message string, value interface{}) FieldError

NewFieldError creates a new FieldError.

func (FieldError) Error

func (e FieldError) Error() string

Error implements the error interface for FieldError.

func (FieldError) String

func (e FieldError) String() string

String returns a string representation of the field error.

type Option

type Option func(interface{})

Option represents a configuration option for validators.

type Schema

type Schema interface {
	// Validate validates a value against this schema.
	// It should add any validation errors to the context.
	// Returns an error only for unexpected failures (not validation failures).
	Validate(value interface{}, ctx *ValidationContext) error

	// Type returns the schema type.
	Type() SchemaType
}

Schema represents a validation schema. All schema types must implement this interface.

func Compile

func Compile(schema Schema) Schema

Compile pre-compiles a schema for better performance when validating multiple times. For v0.1.0, this is a no-op that returns the schema as-is. Future versions will implement actual compilation optimizations.

type SchemaType

type SchemaType string

SchemaType represents the type of a schema.

const (
	// TypeString represents a string schema
	TypeString SchemaType = "string"
	// TypeNumber represents a number schema
	TypeNumber SchemaType = "number"
	// TypeBool represents a boolean schema
	TypeBool SchemaType = "boolean"
	// TypeObject represents an object schema
	TypeObject SchemaType = "object"
	// TypeArray represents an array schema
	TypeArray SchemaType = "array"
	// TypeAny represents a schema that accepts any type
	TypeAny SchemaType = "any"
	// TypeCustom represents a custom validator
	TypeCustom SchemaType = "custom"
	// TypeComposite represents a composite schema (AND/OR/NOT)
	TypeComposite SchemaType = "composite"
	// TypeDateTime represents a date/time schema
	TypeDateTime SchemaType = "datetime"
	// TypeDependent represents a dependent field schema
	TypeDependent SchemaType = "dependent"
	// TypeTransform represents a transformation schema
	TypeTransform SchemaType = "transform"
)

func (SchemaType) String

func (t SchemaType) String() string

String returns the string representation of a SchemaType.

type TransformableSchema added in v0.2.0

type TransformableSchema interface {
	Schema
	// ValidateAndTransform returns the transformed value and any validation error
	ValidateAndTransform(value interface{}, ctx *ValidationContext) (interface{}, error)
}

TransformableSchema represents a schema that can apply transformations.

type TransformationRecord added in v0.2.0

type TransformationRecord struct {
	Path     string
	Original interface{}
	Result   interface{}
	Type     string
}

TransformationRecord records a transformation that was applied.

type Transformer added in v0.2.0

type Transformer interface {
	// Transform applies the transformation to a value
	Transform(value interface{}) (interface{}, error)
}

Transformer represents a function that can transform values. This is defined here to avoid circular dependencies.

type TransformerFunc added in v0.2.0

type TransformerFunc func(value interface{}) (interface{}, error)

TransformerFunc is a function that transforms a value. It returns the transformed value and any error.

type ValidationContext

type ValidationContext struct {
	// contains filtered or unexported fields
}

ValidationContext maintains state during validation. It tracks the current path and accumulates errors.

func NewValidationContext

func NewValidationContext(mode ValidationMode) *ValidationContext

NewValidationContext creates a new validation context.

func (*ValidationContext) AddError

func (c *ValidationContext) AddError(message string, value interface{})

AddError adds an error at the current path.

func (*ValidationContext) AddFieldError

func (c *ValidationContext) AddFieldError(err FieldError)

AddFieldError adds a pre-constructed field error.

func (*ValidationContext) CurrentPath

func (c *ValidationContext) CurrentPath() string

CurrentPath returns the current field path as a string.

func (*ValidationContext) Error

func (c *ValidationContext) Error() error

Error returns a ValidationError if there are any errors, nil otherwise.

func (*ValidationContext) Errors

func (c *ValidationContext) Errors() []FieldError

Errors returns all accumulated errors.

func (*ValidationContext) HasErrors

func (c *ValidationContext) HasErrors() bool

HasErrors returns true if any errors have been added.

func (*ValidationContext) HasTransformations added in v0.2.0

func (c *ValidationContext) HasTransformations() bool

HasTransformations returns true if any transformations were applied.

func (*ValidationContext) Mode

Mode returns the validation mode.

func (*ValidationContext) PopPath

func (c *ValidationContext) PopPath()

PopPath removes the last path segment.

func (*ValidationContext) PushIndex

func (c *ValidationContext) PushIndex(index int)

PushIndex adds an array index to the current path.

func (*ValidationContext) PushPath

func (c *ValidationContext) PushPath(segment string)

PushPath adds a path segment to the current path.

func (*ValidationContext) RecordTransformation added in v0.2.0

func (c *ValidationContext) RecordTransformation(original, result interface{}, transformType string)

RecordTransformation records that a transformation was applied.

func (*ValidationContext) Transformations added in v0.2.0

func (c *ValidationContext) Transformations() []TransformationRecord

Transformations returns all recorded transformations.

func (*ValidationContext) WithIndex

func (c *ValidationContext) WithIndex(index int, fn func())

WithIndex executes a function with an array index pushed onto the context. The index is automatically popped when the function returns.

func (*ValidationContext) WithPath

func (c *ValidationContext) WithPath(segment string, fn func())

WithPath executes a function with a path segment pushed onto the context. The path is automatically popped when the function returns.

type ValidationError

type ValidationError struct {
	Errors []FieldError
}

ValidationError represents one or more validation failures. It contains a slice of FieldError that provides detailed information about each validation failure.

func NewValidationError

func NewValidationError(errors ...FieldError) *ValidationError

NewValidationError creates a new ValidationError with the given field errors.

func (*ValidationError) Add

func (e *ValidationError) Add(path, message string, value interface{})

Add adds a field error to the validation error.

func (*ValidationError) AddError

func (e *ValidationError) AddError(err FieldError)

AddError adds an existing FieldError to the validation error.

func (*ValidationError) Error

func (e *ValidationError) Error() string

Error returns a string representation of all validation errors.

func (*ValidationError) HasErrors

func (e *ValidationError) HasErrors() bool

HasErrors returns true if there are any validation errors.

type ValidationMode

type ValidationMode int

ValidationMode determines how strict the validation is.

const (
	// Strict mode requires exact schema compliance.
	// Extra fields in objects will cause validation to fail.
	Strict ValidationMode = iota

	// Loose mode allows extra fields and safe type coercion.
	// Extra fields in objects are ignored.
	// Safe type coercions are applied (e.g., "123" -> 123).
	Loose
)

func (ValidationMode) String

func (m ValidationMode) String() string

String returns the string representation of a ValidationMode.

type Validator

type Validator struct {
	// contains filtered or unexported fields
}

Validator wraps a schema with configuration options.

func NewValidator

func NewValidator(schema Schema) *Validator

NewValidator creates a new validator with a schema. The validator can be configured with different modes and options.

func (*Validator) Loose

func (v *Validator) Loose() *Validator

Loose sets the validator to loose mode.

func (*Validator) Strict

func (v *Validator) Strict() *Validator

Strict sets the validator to strict mode.

func (*Validator) Validate

func (v *Validator) Validate(data interface{}) error

Validate validates data against the schema.

type ValidatorFunc

type ValidatorFunc func(value interface{}) error

ValidatorFunc is a function that validates a value. It should return an error if validation fails, nil otherwise.

type WithTransform added in v0.2.0

type WithTransform struct {
	Schema
	// contains filtered or unexported fields
}

WithTransform wraps a schema to add transformation capability.

func NewWithTransform added in v0.2.0

func NewWithTransform(schema Schema) *WithTransform

NewWithTransform creates a new schema with transformation support.

func (*WithTransform) AddTransformer added in v0.2.0

func (s *WithTransform) AddTransformer(t TransformerFunc) *WithTransform

AddTransformer adds a transformer to the pipeline.

func (*WithTransform) Type added in v0.2.0

func (s *WithTransform) Type() SchemaType

Type returns the underlying schema type.

func (*WithTransform) Validate added in v0.2.0

func (s *WithTransform) Validate(value interface{}, ctx *ValidationContext) error

Validate applies transformations then validates.

func (*WithTransform) ValidateAndTransform added in v0.2.0

func (s *WithTransform) ValidateAndTransform(value interface{}, ctx *ValidationContext) (interface{}, error)

ValidateAndTransform validates and returns the transformed value.

Directories

Path Synopsis
datetime.go - Date/Time validation builder for Queryfy
datetime.go - Date/Time validation builder for Queryfy
transformers
common.go - Common transformation functions
common.go - Common transformation functions
examples
basic command
datetime command
internal
cache
Package cache provides a simple thread-safe cache implementation.
Package cache provides a simple thread-safe cache implementation.
typeutil
Package typeutil provides type checking and conversion utilities.
Package typeutil provides type checking and conversion utilities.
Package query provides a simple query language for navigating data structures.
Package query provides a simple query language for navigating data structures.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL