✨ feat: implement input validation for API v2
- Added go-playground/validator dependency - Created pkg/validation/ package with custom validator wrapper - Implemented request validation for v2 greet endpoint - Added structured validation error responses - Extended BDD tests to cover validation scenarios - Updated AGENTS.md with v2 API documentation - Created ADR 0011-validation-library-selection.md - Simplified server handler creation code - Updated CHANGELOG with implementation details
This commit is contained in:
44
AGENTS.md
44
AGENTS.md
@@ -388,6 +388,50 @@ curl http://localhost:8080/api/v1/greet/Alice
|
|||||||
# Response: {"message":"Hello Alice!"}
|
# Response: {"message":"Hello Alice!"}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Greet Service v2 (Feature Flag Enabled)
|
||||||
|
```http
|
||||||
|
POST /api/v2/greet
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "John"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
# Valid request
|
||||||
|
curl -X POST http://localhost:8080/api/v2/greet \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"John"}'
|
||||||
|
# Response: {"message":"Hello my friend John!"}
|
||||||
|
|
||||||
|
# Empty name (valid, returns default)
|
||||||
|
curl -X POST http://localhost:8080/api/v2/greet \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":""}'
|
||||||
|
# Response: {"message":"Hello my friend!"}
|
||||||
|
|
||||||
|
# Missing name field (valid, returns default)
|
||||||
|
curl -X POST http://localhost:8080/api/v2/greet \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{}'
|
||||||
|
# Response: {"message":"Hello my friend!"}
|
||||||
|
|
||||||
|
# Name too long (validation error)
|
||||||
|
curl -X POST http://localhost:8080/api/v2/greet \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"ThisNameIsWayTooLongAndShouldFailValidationBecauseItExceedsTheMaximumAllowedLengthOf100Characters!!!!"}'
|
||||||
|
# Response: {"error":"validation_failed","message":"Invalid request data","details":[{"message":"Name failed validation for 'max' (parameter: 100)"}]}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validation Rules:**
|
||||||
|
- `name`: Maximum length 100 characters (optional field)
|
||||||
|
|
||||||
|
**Feature Flag:** Enable with `DLC_API_V2_ENABLED=true` or in config file with `api.v2_enabled: true`
|
||||||
|
|
||||||
## 🔗 OpenTelemetry & Jaeger Integration
|
## 🔗 OpenTelemetry & Jaeger Integration
|
||||||
|
|
||||||
The application supports OpenTelemetry for distributed tracing with Jaeger compatibility.
|
The application supports OpenTelemetry for distributed tracing with Jaeger compatibility.
|
||||||
|
|||||||
11
CHANGELOG.md
11
CHANGELOG.md
@@ -62,6 +62,17 @@ vibe start --agent dancelessonscoachprogrammer
|
|||||||
- ✅ Updated configuration system to support API versioning
|
- ✅ Updated configuration system to support API versioning
|
||||||
- ✅ Added comprehensive test coverage for both enabled and disabled states
|
- ✅ Added comprehensive test coverage for both enabled and disabled states
|
||||||
|
|
||||||
|
### 2026-04-04 - Input Validation Implementation
|
||||||
|
- ✅ Selected go-playground/validator for input validation
|
||||||
|
- ✅ Created ADR [0011-validation-library-selection.md](adr/0011-validation-library-selection.md)
|
||||||
|
- ✅ Added `pkg/validation/` package with custom validator wrapper
|
||||||
|
- ✅ Implemented request validation for v2 API endpoints
|
||||||
|
- ✅ Added structured validation error responses
|
||||||
|
- ✅ Extended BDD tests to cover validation scenarios
|
||||||
|
- ✅ Added validation for name field (max length: 100 characters)
|
||||||
|
- ✅ Maintained graceful degradation when validator fails to initialize
|
||||||
|
- ⚠️ **REMINDER**: Use `./scripts/build.sh` instead of `go build` directly for consistent builds
|
||||||
|
|
||||||
## Compact History (Last 5 Entries)
|
## Compact History (Last 5 Entries)
|
||||||
|
|
||||||
### 2026-04-04
|
### 2026-04-04
|
||||||
|
|||||||
198
adr/0011-validation-library-selection.md
Normal file
198
adr/0011-validation-library-selection.md
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
# 11. Validation Library Selection
|
||||||
|
|
||||||
|
**Date:** 2026-04-04
|
||||||
|
**Status:** Proposed
|
||||||
|
**Authors:** AI Agent
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The DanceLessonsCoach project needs to add input validation for API requests, particularly for the new v2 API endpoints that accept JSON payloads. Currently, there is no structured validation in place, which could lead to invalid data being processed by the system.
|
||||||
|
|
||||||
|
## Decision Drivers
|
||||||
|
|
||||||
|
1. **Maturity and Stability**: Need a well-established library with proven track record
|
||||||
|
2. **Community Support**: Active maintenance and community adoption
|
||||||
|
3. **Feature Completeness**: Support for common validation scenarios (required fields, string lengths, numeric ranges, etc.)
|
||||||
|
4. **Performance**: Minimal impact on request processing times
|
||||||
|
5. **Integration**: Easy to integrate with existing Chi router and JSON handling
|
||||||
|
6. **Error Handling**: Clear, actionable error messages for API consumers
|
||||||
|
7. **Extensibility**: Ability to add custom validation rules
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
### 1. go-playground/validator (v10)
|
||||||
|
|
||||||
|
**Overview:** The most widely adopted validation library for Go, using struct tags for rule definition.
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- ✅ **Most mature and stable** - Used in production by thousands of projects
|
||||||
|
- ✅ **Extensive built-in validators** - Covers 90%+ of common validation needs
|
||||||
|
- ✅ **Large community** - Active GitHub repository with frequent updates
|
||||||
|
- ✅ **Good documentation** - Comprehensive examples and guides
|
||||||
|
- ✅ **Struct tag-based** - Clean separation of validation rules from business logic
|
||||||
|
- ✅ **Custom validators** - Support for adding project-specific validation rules
|
||||||
|
- ✅ **Cross-field validation** - Can validate relationships between fields
|
||||||
|
- ✅ **JSON Schema generation** - Can generate schemas from validation tags
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- ❌ **Reflection-based** - Slightly slower than compile-time alternatives
|
||||||
|
- ❌ **Tag syntax** - Can become verbose for complex validations
|
||||||
|
- ❌ **Error messages** - Requires some customization for API-friendly errors
|
||||||
|
|
||||||
|
### 2. ozzo-validation
|
||||||
|
|
||||||
|
**Overview:** Configurable and extensible data validation using code-based rules.
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- ✅ **Code-based validation** - Rules defined in Go code rather than tags
|
||||||
|
- ✅ **Customizable errors** - Better control over error message formatting
|
||||||
|
- ✅ **Extensible** - Easy to add new validation rules
|
||||||
|
- ✅ **Good performance** - Faster than reflection-based validators
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- ❌ **Less mature** - Smaller community than go-playground/validator
|
||||||
|
- ❌ **More verbose** - Requires more code for common validations
|
||||||
|
- ❌ **Learning curve** - Different approach than tag-based validation
|
||||||
|
|
||||||
|
### 3. Valgo
|
||||||
|
|
||||||
|
**Overview:** Type-safe, expressive, and extensible validator library.
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- ✅ **Type-safe** - Compile-time type checking
|
||||||
|
- ✅ **Modern API** - Clean, expressive syntax
|
||||||
|
- ✅ **Good performance** - Type-safe approach can be faster
|
||||||
|
- ✅ **Extensible** - Easy to add custom validators
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- ❌ **Newer library** - Less battle-tested than go-playground/validator
|
||||||
|
- ❌ **Smaller community** - Fewer resources and examples available
|
||||||
|
- ❌ **Breaking changes** - Still evolving API
|
||||||
|
|
||||||
|
### 4. govalid
|
||||||
|
|
||||||
|
**Overview:** Compile-time validation library that generates validation code.
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- ✅ **Compile-time generation** - Up to 45x faster than reflection-based
|
||||||
|
- ✅ **No reflection overhead** - Better performance in hot paths
|
||||||
|
- ✅ **Type-safe** - Compile-time checking
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- ❌ **Build complexity** - Requires code generation step
|
||||||
|
- ❌ **Less flexible** - Harder to add runtime validation rules
|
||||||
|
- ❌ **Smaller ecosystem** - Fewer built-in validators
|
||||||
|
|
||||||
|
## Decision Outcome
|
||||||
|
|
||||||
|
**Chosen option:** `go-playground/validator` (v10)
|
||||||
|
|
||||||
|
**Rationale:**
|
||||||
|
|
||||||
|
1. **Proven Track Record**: Used successfully in countless production Go applications
|
||||||
|
2. **Community Support**: Large ecosystem, active maintenance, and extensive documentation
|
||||||
|
3. **Feature Completeness**: Covers all our current and anticipated validation needs
|
||||||
|
4. **Integration**: Works seamlessly with our existing struct-based JSON handling
|
||||||
|
5. **Performance**: While not the fastest, the performance impact is negligible for our use case
|
||||||
|
6. **Error Handling**: Can be customized to provide API-friendly error messages
|
||||||
|
7. **Extensibility**: Supports custom validators for project-specific needs
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### Phase 1: Integration Setup
|
||||||
|
1. Add `github.com/go-playground/validator/v10` dependency
|
||||||
|
2. Create validation utility package in `pkg/validation/`
|
||||||
|
3. Set up validator instance with custom error handling
|
||||||
|
4. Add common validation tags and error message mappings
|
||||||
|
|
||||||
|
### Phase 2: API v2 Validation
|
||||||
|
1. Add validation to `greetRequest` struct in `api_v2.go`
|
||||||
|
2. Implement request validation middleware
|
||||||
|
3. Create custom error responses for validation failures
|
||||||
|
4. Add comprehensive validation tests
|
||||||
|
|
||||||
|
### Phase 3: Extend to Other Endpoints
|
||||||
|
1. Apply validation to existing v1 endpoints (optional)
|
||||||
|
2. Add validation to health/readiness endpoints (if needed)
|
||||||
|
3. Create validation documentation for API consumers
|
||||||
|
|
||||||
|
### Phase 4: Advanced Features
|
||||||
|
1. Add custom validators for business rules
|
||||||
|
2. Implement internationalized error messages
|
||||||
|
3. Add validation performance monitoring
|
||||||
|
|
||||||
|
## Validation Strategy
|
||||||
|
|
||||||
|
### Request Validation Pattern
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Define validated struct
|
||||||
|
type GreetRequest struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,max=100"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate in handler
|
||||||
|
func (h *apiV2GreetHandler) handleGreetPost(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req GreetRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
// Handle JSON decode error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validator.Validate(req); err != nil {
|
||||||
|
// Return validation error response
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process valid request
|
||||||
|
message := h.greeter.GreetV2(r.Context(), req.Name)
|
||||||
|
h.writeJSONResponse(w, message)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Response Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "validation_failed",
|
||||||
|
"message": "Invalid request data",
|
||||||
|
"details": [
|
||||||
|
{
|
||||||
|
"field": "name",
|
||||||
|
"error": "required",
|
||||||
|
"message": "Name is required"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Path
|
||||||
|
|
||||||
|
1. **Initial Integration**: Add validator to v2 endpoints only
|
||||||
|
2. **Testing**: Validate performance and error handling
|
||||||
|
3. **Documentation**: Update API docs with validation requirements
|
||||||
|
4. **Gradual Rollout**: Apply to other endpoints as needed
|
||||||
|
5. **Monitoring**: Track validation failures and adjust rules
|
||||||
|
|
||||||
|
## Future Considerations
|
||||||
|
|
||||||
|
- **Performance Optimization**: If validation becomes bottleneck, consider compile-time alternatives
|
||||||
|
- **Schema Generation**: Generate OpenAPI schemas from validation tags
|
||||||
|
- **Internationalization**: Support multiple languages for error messages
|
||||||
|
- **Rule Management**: Externalize validation rules for dynamic configuration
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [go-playground/validator GitHub](https://github.com/go-playground/validator)
|
||||||
|
- [Validator Documentation](https://pkg.go.dev/github.com/go-playground/validator/v10)
|
||||||
|
- [Go Validation Libraries Comparison](https://leapcell.io/blog/exploring-golang-s-validation-libraries)
|
||||||
|
|
||||||
|
## Changelog Entry
|
||||||
|
|
||||||
|
```
|
||||||
|
### 2026-04-04 - Validation Library Selection
|
||||||
|
- ✅ Selected go-playground/validator for input validation
|
||||||
|
- ✅ Created ADR 0011-validation-library-selection.md
|
||||||
|
- ✅ Planned integration strategy for API validation
|
||||||
|
- ✅ Designed error response format for validation failures
|
||||||
|
```
|
||||||
@@ -69,6 +69,7 @@ Chosen option: "[Option 1]" because [justification]
|
|||||||
* [0008-bdd-testing.md](0008-bdd-testing.md) - Adopt BDD with Godog for behavioral testing
|
* [0008-bdd-testing.md](0008-bdd-testing.md) - Adopt BDD with Godog for behavioral testing
|
||||||
* [0009-hybrid-testing-approach.md](0009-hybrid-testing-approach.md) - Combine BDD and Swagger-based testing
|
* [0009-hybrid-testing-approach.md](0009-hybrid-testing-approach.md) - Combine BDD and Swagger-based testing
|
||||||
* [0010-api-v2-feature-flag.md](0010-api-v2-feature-flag.md) - API v2 implementation with feature flag control
|
* [0010-api-v2-feature-flag.md](0010-api-v2-feature-flag.md) - API v2 implementation with feature flag control
|
||||||
|
* [0011-validation-library-selection.md](0011-validation-library-selection.md) - Selection of go-playground/validator for input validation
|
||||||
|
|
||||||
## How to Add a New ADR
|
## How to Add a New ADR
|
||||||
|
|
||||||
|
|||||||
@@ -20,4 +20,14 @@ Feature: Greet Service
|
|||||||
Scenario: v2 default greeting with empty name
|
Scenario: v2 default greeting with empty name
|
||||||
Given the server is running with v2 enabled
|
Given the server is running with v2 enabled
|
||||||
When I send a POST request to v2 greet with name ""
|
When I send a POST request to v2 greet with name ""
|
||||||
Then the response should be "{\"message\":\"Hello my friend!\"}"
|
Then the response should be "{\"message\":\"Hello my friend!\"}"
|
||||||
|
|
||||||
|
Scenario: v2 greeting with missing name field
|
||||||
|
Given the server is running with v2 enabled
|
||||||
|
When I send a POST request to v2 greet with invalid JSON "{}"
|
||||||
|
Then the response should be "{\"message\":\"Hello my friend!\"}"
|
||||||
|
|
||||||
|
Scenario: v2 greeting with name that is too long
|
||||||
|
Given the server is running with v2 enabled
|
||||||
|
When I send a POST request to v2 greet with name "ThisNameIsWayTooLongAndShouldFailValidationBecauseItExceedsTheMaximumAllowedLengthOf100Characters!!!!"
|
||||||
|
Then the response should contain error "validation_failed"
|
||||||
6
go.mod
6
go.mod
@@ -25,8 +25,12 @@ require (
|
|||||||
github.com/cucumber/messages/go/v21 v21.0.1 // indirect
|
github.com/cucumber/messages/go/v21 v21.0.1 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.30.2 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||||
github.com/gogo/protobuf v1.3.1 // indirect
|
github.com/gogo/protobuf v1.3.1 // indirect
|
||||||
@@ -35,6 +39,7 @@ require (
|
|||||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||||
github.com/hashicorp/go-memdb v1.3.5 // indirect
|
github.com/hashicorp/go-memdb v1.3.5 // indirect
|
||||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
@@ -49,6 +54,7 @@ require (
|
|||||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
|
golang.org/x/crypto v0.49.0 // indirect
|
||||||
golang.org/x/net v0.52.0 // indirect
|
golang.org/x/net v0.52.0 // indirect
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
golang.org/x/text v0.35.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
|
|||||||
12
go.sum
12
go.sum
@@ -75,6 +75,8 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z
|
|||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||||
@@ -87,6 +89,12 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
|||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
@@ -190,6 +198,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
@@ -324,6 +334,8 @@ golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnf
|
|||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ func InitializeAllSteps(ctx *godog.ScenarioContext, client *testserver.Client) {
|
|||||||
ctx.Step(`^the server is running$`, sc.theServerIsRunning)
|
ctx.Step(`^the server is running$`, sc.theServerIsRunning)
|
||||||
ctx.Step(`^the server is running with v2 enabled$`, sc.theServerIsRunningWithV2Enabled)
|
ctx.Step(`^the server is running with v2 enabled$`, sc.theServerIsRunningWithV2Enabled)
|
||||||
ctx.Step(`^I send a POST request to v2 greet with name "([^"]*)"$`, sc.iSendPOSTRequestToV2GreetWithName)
|
ctx.Step(`^I send a POST request to v2 greet with name "([^"]*)"$`, sc.iSendPOSTRequestToV2GreetWithName)
|
||||||
|
ctx.Step(`^I send a POST request to v2 greet with invalid JSON "([^"]*)"$`, sc.iSendPOSTRequestToV2GreetWithInvalidJSON)
|
||||||
|
ctx.Step(`^the response should contain error "([^"]*)"$`, sc.theResponseShouldContainError)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *StepContext) iRequestAGreetingFor(name string) error {
|
func (sc *StepContext) iRequestAGreetingFor(name string) error {
|
||||||
@@ -91,3 +93,17 @@ func (sc *StepContext) iSendPOSTRequestToV2GreetWithName(name string) error {
|
|||||||
requestBody := map[string]string{"name": name}
|
requestBody := map[string]string{"name": name}
|
||||||
return sc.client.Request("POST", "/api/v2/greet", requestBody)
|
return sc.client.Request("POST", "/api/v2/greet", requestBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sc *StepContext) iSendPOSTRequestToV2GreetWithInvalidJSON(invalidJSON string) error {
|
||||||
|
// Send raw invalid JSON
|
||||||
|
return sc.client.Request("POST", "/api/v2/greet", invalidJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *StepContext) theResponseShouldContainError(expectedError string) error {
|
||||||
|
// Check if the response contains the expected error
|
||||||
|
body := string(sc.client.GetLastBody())
|
||||||
|
if !strings.Contains(body, expectedError) {
|
||||||
|
return fmt.Errorf("expected response to contain error %q, got %q", expectedError, body)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ package greet
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"DanceLessonsCoach/pkg/validation"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
@@ -19,11 +22,12 @@ type ApiV2Greet interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type apiV2GreetHandler struct {
|
type apiV2GreetHandler struct {
|
||||||
greeter GreeterV2
|
greeter GreeterV2
|
||||||
|
validator *validation.Validator
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApiV2GreetHandler(greeter GreeterV2) ApiV2Greet {
|
func NewApiV2GreetHandler(greeter GreeterV2, validator *validation.Validator) ApiV2Greet {
|
||||||
return &apiV2GreetHandler{greeter: greeter}
|
return &apiV2GreetHandler{greeter: greeter, validator: validator}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *apiV2GreetHandler) RegisterRoutes(router chi.Router) {
|
func (h *apiV2GreetHandler) RegisterRoutes(router chi.Router) {
|
||||||
@@ -33,7 +37,7 @@ func (h *apiV2GreetHandler) RegisterRoutes(router chi.Router) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type greetRequest struct {
|
type greetRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name" validate:"max=100"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type greetResponse struct {
|
type greetResponse struct {
|
||||||
@@ -55,6 +59,17 @@ func (h *apiV2GreetHandler) handleGreetPost(w http.ResponseWriter, r *http.Reque
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate request if validator is available
|
||||||
|
if h.validator != nil {
|
||||||
|
log.Trace().Str("name", req.Name).Msg("Validating request")
|
||||||
|
if err := h.validator.Validate(req); err != nil {
|
||||||
|
log.Trace().Err(err).Msg("Validation failed")
|
||||||
|
h.handleValidationError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Trace().Msg("Validation passed")
|
||||||
|
}
|
||||||
|
|
||||||
// Call service
|
// Call service
|
||||||
message := h.greeter.GreetV2(r.Context(), req.Name)
|
message := h.greeter.GreetV2(r.Context(), req.Name)
|
||||||
|
|
||||||
@@ -62,6 +77,34 @@ func (h *apiV2GreetHandler) handleGreetPost(w http.ResponseWriter, r *http.Reque
|
|||||||
h.writeJSONResponse(w, message)
|
h.writeJSONResponse(w, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *apiV2GreetHandler) handleValidationError(w http.ResponseWriter, err error) {
|
||||||
|
var validationErr *validation.ValidationError
|
||||||
|
if errors.As(err, &validationErr) {
|
||||||
|
// Create structured validation error response
|
||||||
|
response := map[string]interface{}{
|
||||||
|
"error": "validation_failed",
|
||||||
|
"message": "Invalid request data",
|
||||||
|
"details": make([]map[string]string, 0, len(validationErr.Messages)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse validation messages into structured format
|
||||||
|
for _, msg := range validationErr.Messages {
|
||||||
|
// Simple parsing - in production, use proper parsing
|
||||||
|
detail := map[string]string{
|
||||||
|
"message": msg,
|
||||||
|
}
|
||||||
|
response["details"] = append(response["details"].([]map[string]string), detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
} else {
|
||||||
|
// Fallback for other types of errors
|
||||||
|
http.Error(w, `{"error":"validation_error","message":`+strconv.Quote(err.Error())+`}`, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *apiV2GreetHandler) writeJSONResponse(w http.ResponseWriter, message string) {
|
func (h *apiV2GreetHandler) writeJSONResponse(w http.ResponseWriter, message string) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(greetResponse{Message: message})
|
json.NewEncoder(w).Encode(greetResponse{Message: message})
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"DanceLessonsCoach/pkg/config"
|
"DanceLessonsCoach/pkg/config"
|
||||||
"DanceLessonsCoach/pkg/greet"
|
"DanceLessonsCoach/pkg/greet"
|
||||||
|
"DanceLessonsCoach/pkg/validation"
|
||||||
"DanceLessonsCoach/pkg/telemetry"
|
"DanceLessonsCoach/pkg/telemetry"
|
||||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||||
@@ -20,19 +21,29 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
router *chi.Mux
|
router *chi.Mux
|
||||||
readyCtx context.Context
|
readyCtx context.Context
|
||||||
withOTEL bool
|
withOTEL bool
|
||||||
config *config.Config
|
config *config.Config
|
||||||
tracerProvider *sdktrace.TracerProvider
|
tracerProvider *sdktrace.TracerProvider
|
||||||
|
validator *validation.Validator
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(cfg *config.Config, readyCtx context.Context) *Server {
|
func NewServer(cfg *config.Config, readyCtx context.Context) *Server {
|
||||||
|
// Create validator instance
|
||||||
|
validator, err := validation.GetValidatorFromConfig(cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to create validator, continuing without validation")
|
||||||
|
} else {
|
||||||
|
log.Trace().Msg("Validator created successfully")
|
||||||
|
}
|
||||||
|
|
||||||
s := &Server{
|
s := &Server{
|
||||||
router: chi.NewRouter(),
|
router: chi.NewRouter(),
|
||||||
readyCtx: readyCtx,
|
readyCtx: readyCtx,
|
||||||
withOTEL: cfg.GetTelemetryEnabled(),
|
withOTEL: cfg.GetTelemetryEnabled(),
|
||||||
config: cfg,
|
config: cfg,
|
||||||
|
validator: validator,
|
||||||
}
|
}
|
||||||
s.setupRoutes()
|
s.setupRoutes()
|
||||||
return s
|
return s
|
||||||
@@ -76,7 +87,7 @@ func (s *Server) registerApiV1Routes(r chi.Router) {
|
|||||||
|
|
||||||
func (s *Server) registerApiV2Routes(r chi.Router) {
|
func (s *Server) registerApiV2Routes(r chi.Router) {
|
||||||
greetServiceV2 := greet.NewServiceV2()
|
greetServiceV2 := greet.NewServiceV2()
|
||||||
greetHandlerV2 := greet.NewApiV2GreetHandler(greetServiceV2)
|
greetHandlerV2 := greet.NewApiV2GreetHandler(greetServiceV2, s.validator)
|
||||||
r.Route("/greet", func(r chi.Router) {
|
r.Route("/greet", func(r chi.Router) {
|
||||||
greetHandlerV2.RegisterRoutes(r)
|
greetHandlerV2.RegisterRoutes(r)
|
||||||
})
|
})
|
||||||
|
|||||||
123
pkg/validation/validator.go
Normal file
123
pkg/validation/validator.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"DanceLessonsCoach/pkg/config"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-playground/locales/en"
|
||||||
|
en_translations "github.com/go-playground/validator/v10/translations/en"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
ut "github.com/go-playground/universal-translator"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validator wraps the go-playground/validator with custom error handling
|
||||||
|
type Validator struct {
|
||||||
|
validate *validator.Validate
|
||||||
|
translator ut.Translator
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewValidator creates a new validator instance with custom error handling
|
||||||
|
func NewValidator() (*Validator, error) {
|
||||||
|
// Create validator instance
|
||||||
|
validate := validator.New()
|
||||||
|
|
||||||
|
// Register translations
|
||||||
|
english := en.New()
|
||||||
|
uni := ut.New(english, english)
|
||||||
|
trans, _ := uni.GetTranslator("en")
|
||||||
|
|
||||||
|
// Register translation for default validator
|
||||||
|
if err := en_translations.RegisterDefaultTranslations(validate, trans); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to register translations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register custom validations
|
||||||
|
if err := registerCustomValidations(validate); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to register custom validations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Validator{
|
||||||
|
validate: validate,
|
||||||
|
translator: trans,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates a struct and returns validation errors
|
||||||
|
func (v *Validator) Validate(structObj interface{}) error {
|
||||||
|
if structObj == nil {
|
||||||
|
return errors.New("validation: nil object provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the type of the struct
|
||||||
|
structType := reflect.TypeOf(structObj)
|
||||||
|
if structType.Kind() != reflect.Struct {
|
||||||
|
return fmt.Errorf("validation: expected struct, got %s", structType.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform validation
|
||||||
|
if err := v.validate.Struct(structObj); err != nil {
|
||||||
|
if _, ok := err.(*validator.InvalidValidationError); ok {
|
||||||
|
return fmt.Errorf("validation: invalid validation error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert validation errors to custom format
|
||||||
|
validationErrors := err.(validator.ValidationErrors)
|
||||||
|
return v.formatValidationErrors(validationErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatValidationErrors converts validator.ValidationErrors to a custom error type
|
||||||
|
func (v *Validator) formatValidationErrors(errors validator.ValidationErrors) error {
|
||||||
|
var errorMessages []string
|
||||||
|
|
||||||
|
for _, err := range errors {
|
||||||
|
field := err.Field()
|
||||||
|
tag := err.Tag()
|
||||||
|
param := err.Param()
|
||||||
|
|
||||||
|
// Get custom error message
|
||||||
|
message := fmt.Errorf("%s failed validation for '%s'", field, tag).Error()
|
||||||
|
|
||||||
|
// Add parameter if available
|
||||||
|
if param != "" {
|
||||||
|
message += fmt.Sprintf(" (parameter: %s)", param)
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMessages = append(errorMessages, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ValidationError{
|
||||||
|
Messages: errorMessages,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidationError represents multiple validation errors
|
||||||
|
type ValidationError struct {
|
||||||
|
Messages []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ValidationError) Error() string {
|
||||||
|
return strings.Join(e.Messages, "; ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerCustomValidations registers any custom validation functions
|
||||||
|
func registerCustomValidations(validate *validator.Validate) error {
|
||||||
|
// Add custom validations here as needed
|
||||||
|
// Example:
|
||||||
|
// if err := validate.RegisterValidation("custom_tag", customValidationFunc); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValidatorFromConfig creates a validator instance based on application config
|
||||||
|
func GetValidatorFromConfig(cfg *config.Config) (*Validator, error) {
|
||||||
|
// For now, config doesn't affect validator creation
|
||||||
|
// But this allows future configuration (e.g., language, strict mode)
|
||||||
|
return NewValidator()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user