Remove github.com/google/uuid dependency and update module files
- Deleted the `github.com/google/uuid` dependency from `go.mod` to streamline the project and reduce unnecessary dependencies. - Updated `go.sum` to reflect the removal of the UUID package. - Enhanced the overall module management by ensuring only necessary dependencies are included.
This commit is contained in:
@@ -45,106 +45,113 @@ This system implements **Clean Architecture** with:
|
|||||||
- **Input Validation**: Govalidator integration for request validation
|
- **Input Validation**: Govalidator integration for request validation
|
||||||
- **Error Handling**: Consistent error responses and logging
|
- **Error Handling**: Consistent error responses and logging
|
||||||
- **Time Handling**: Unix timestamps throughout the application
|
- **Time Handling**: Unix timestamps throughout the application
|
||||||
- **API Documentation**: Auto-generated Swagger documentation
|
- **Generic XML Parser**: Parse any XML structure into Go structs using generics
|
||||||
|
|
||||||
## 📋 API Documentation
|
## 🔧 New Features
|
||||||
|
|
||||||
### Swagger/OpenAPI 3.0 Documentation
|
### Generic XML Parser
|
||||||
|
|
||||||
The Tender Management API features comprehensive, auto-generated Swagger documentation with:
|
A powerful, generic XML parser built with Go generics that can parse any XML structure into Go structs. Perfect for handling complex XML formats like TED (Tenders Electronic Daily) notices.
|
||||||
|
|
||||||
#### 🚀 **Version 2.0.0 Features**
|
#### Features
|
||||||
- **Comprehensive API Coverage**: All endpoints documented with detailed descriptions, examples, and error responses
|
- **Generic Support**: Parse any XML structure that implements the `Parseable` interface
|
||||||
- **Interactive API Testing**: Built-in Swagger UI for testing endpoints directly from the documentation
|
- **Flexible Configuration**: Customizable parsing options for different use cases
|
||||||
- **Authentication Integration**: Bearer token authentication examples and testing capability
|
- **Built-in Validation**: Automatic validation of parsed structures
|
||||||
- **Request/Response Examples**: Real-world examples for all data structures and API calls
|
- **Metadata Extraction**: Extract XML metadata and parsing information
|
||||||
- **Error Documentation**: Detailed error codes, messages, and troubleshooting information
|
- **TED XML Support**: Pre-built models for TED contract notices
|
||||||
|
- **Error Reporting**: Detailed error and warning reporting
|
||||||
|
|
||||||
#### 📊 **API Endpoints Categories**
|
#### Quick Example
|
||||||
- **Health**: System monitoring and health check endpoints
|
```go
|
||||||
- **Authorization**: User authentication, token management, and session handling
|
// Define your XML structure
|
||||||
- **Users**: Administrative user management with RBAC (Role-Based Access Control)
|
type Contract struct {
|
||||||
- **Admin-Customers**: Web panel customer management with advanced filtering
|
XMLName xml.Name `xml:"Contract"`
|
||||||
- **Customers-Authorization**: Mobile app customer authentication and profile access
|
ID string `xml:"id,attr"`
|
||||||
- **Admin-Companies**: Comprehensive company management with search, filtering, and analytics
|
Name string `xml:"name"`
|
||||||
|
Value string `xml:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
#### 🔧 **Documentation Access**
|
func (c Contract) Validate() error { /* validation logic */ }
|
||||||
- **Local Development**: `http://localhost:8081/swagger/`
|
func (c Contract) GetID() string { return c.ID }
|
||||||
- **Interactive Testing**: All endpoints can be tested directly from the Swagger UI
|
|
||||||
- **Authentication**: Use the "Authorize" button to set Bearer tokens for protected endpoints
|
|
||||||
- **Export Options**: JSON and YAML formats available for API specifications
|
|
||||||
|
|
||||||
#### 📝 **Enhanced Features**
|
// Parse XML
|
||||||
- **Detailed Descriptions**: Each endpoint includes comprehensive business logic explanations
|
parser := xmlparser.NewParser[Contract](nil)
|
||||||
- **Parameter Validation**: Complete validation rules and constraints for all input parameters
|
contract, err := parser.ParseString(xmlData)
|
||||||
- **Response Examples**: Real-world response examples with proper HTTP status codes
|
|
||||||
- **Security Schemes**: JWT Bearer token authentication clearly documented
|
|
||||||
- **Error Handling**: Comprehensive error response documentation with troubleshooting guides
|
|
||||||
- **Filtering & Pagination**: Advanced query parameters for list endpoints with examples
|
|
||||||
- **Business Logic**: Context-aware descriptions explaining when and how to use each endpoint
|
|
||||||
|
|
||||||
#### 🏗️ **Technical Specifications**
|
|
||||||
- **OpenAPI/Swagger 2.0**: Industry-standard API documentation format
|
|
||||||
- **Auto-Generation**: Documentation automatically updated from code annotations
|
|
||||||
- **Type Safety**: Strong typing with Go struct validation and examples
|
|
||||||
- **Validation Rules**: Complete govalidator integration for request validation
|
|
||||||
- **Consistent Response Format**: Standardized API response structure with metadata
|
|
||||||
|
|
||||||
#### 💡 **Usage Examples**
|
|
||||||
```bash
|
|
||||||
# Access Swagger UI
|
|
||||||
curl http://localhost:8081/swagger/
|
|
||||||
|
|
||||||
# Download API specification
|
|
||||||
curl http://localhost:8081/swagger/doc.json
|
|
||||||
|
|
||||||
# Test authentication endpoint
|
|
||||||
curl -X POST "http://localhost:8081/admin/v1/login" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"username": "admin", "password": "password"}'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 🔐 **Authentication Testing**
|
See [pkg/xmlparser/README.md](pkg/xmlparser/README.md) for complete documentation and examples.
|
||||||
1. Navigate to Swagger UI at `/swagger/`
|
|
||||||
2. Click "Authorize" button
|
|
||||||
3. Enter: `Bearer <your_jwt_token>`
|
|
||||||
4. Test protected endpoints directly from the interface
|
|
||||||
|
|
||||||
### API Design Principles
|
## 📦 Package Structure
|
||||||
- **RESTful Design**: Consistent REST principles with proper HTTP methods and status codes
|
|
||||||
- **Clean Architecture**: Domain-driven design with clear separation of concerns
|
|
||||||
- **Validation**: Comprehensive input validation with meaningful error messages
|
|
||||||
- **Security**: JWT-based authentication with proper token management
|
|
||||||
- **Performance**: Optimized queries with pagination and filtering capabilities
|
|
||||||
- **Maintainability**: Auto-generated documentation stays in sync with code changes
|
|
||||||
|
|
||||||
## 🔧 Development
|
```
|
||||||
|
├── cmd/ # Application entry points
|
||||||
|
├── internal/ # Private application code
|
||||||
|
│ ├── company/ # Company domain
|
||||||
|
│ ├── customer/ # Customer domain
|
||||||
|
│ └── user/ # User domain
|
||||||
|
├── pkg/ # Public packages
|
||||||
|
│ ├── authorization/ # Authorization utilities
|
||||||
|
│ ├── logger/ # Structured logging
|
||||||
|
│ ├── mongo/ # MongoDB utilities
|
||||||
|
│ ├── redis/ # Redis utilities
|
||||||
|
│ ├── response/ # HTTP response utilities
|
||||||
|
│ └── xmlparser/ # Generic XML parser
|
||||||
|
├── infra/ # Infrastructure configuration
|
||||||
|
└── docs/ # Documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Test specific package
|
||||||
|
go test ./pkg/xmlparser/...
|
||||||
|
|
||||||
|
# Test with coverage
|
||||||
|
make test-coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Development
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
- Go 1.23+
|
- Go 1.23+
|
||||||
- MongoDB 4.4+
|
- MongoDB
|
||||||
- Valid MongoDB connection
|
- Redis (optional)
|
||||||
|
|
||||||
### Project Structure
|
### Local Development
|
||||||
```
|
```bash
|
||||||
tm_back/
|
# Clone repository
|
||||||
├── cmd/web/ # Application entry point
|
git clone <repository-url>
|
||||||
├── internal/ # Private application code
|
cd tm_back
|
||||||
│ ├── customer/ # Customer domain
|
|
||||||
│ └── ... # Other domains
|
# Install dependencies
|
||||||
├── pkg/ # Public packages
|
go mod tidy
|
||||||
│ ├── logger/ # Logging utilities
|
|
||||||
│ ├── mongo/ # MongoDB utilities
|
# Run tests
|
||||||
│ └── response/ # API response utilities
|
make test
|
||||||
├── docs/ # 📚 All documentation
|
|
||||||
└── config.yaml # Configuration file
|
# Start development server
|
||||||
|
make dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📞 Support
|
### Adding New Features
|
||||||
|
1. Follow Clean Architecture principles
|
||||||
|
2. Implement proper logging in service layer
|
||||||
|
3. Add comprehensive validation
|
||||||
|
4. Write unit tests
|
||||||
|
5. Update documentation
|
||||||
|
|
||||||
For detailed documentation, guides, and examples, please visit the [`docs/`](./docs/) directory.
|
## 📝 Contributing
|
||||||
|
|
||||||
---
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Follow coding standards
|
||||||
|
4. Add tests for new features
|
||||||
|
5. Update documentation
|
||||||
|
6. Submit a pull request
|
||||||
|
|
||||||
**Version**: 1.0.0
|
## 📄 License
|
||||||
**Last Updated**: $(date)
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
Binary file not shown.
@@ -6,7 +6,6 @@ toolchain go1.24.4
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
github.com/google/uuid v1.4.0
|
|
||||||
github.com/labstack/echo/v4 v4.13.4
|
github.com/labstack/echo/v4 v4.13.4
|
||||||
github.com/redis/go-redis/v9 v9.12.0
|
github.com/redis/go-redis/v9 v9.12.0
|
||||||
github.com/spf13/viper v1.18.2
|
github.com/spf13/viper v1.18.2
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
|||||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
|
||||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
|||||||
@@ -0,0 +1,307 @@
|
|||||||
|
# Generic XML Parser Package
|
||||||
|
|
||||||
|
This package is a generic XML parser implemented using Go Generics that can parse any XML structure into Go structs.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Generic**: Support for any struct type that implements the `Parseable` interface
|
||||||
|
- **Flexible**: Configurable parsing options for different use cases
|
||||||
|
- **Validation**: Automatic validation of XML and parsed structures
|
||||||
|
- **Metadata Extraction**: Extract XML metadata and parsing information
|
||||||
|
- **Advanced Error Handling**: Detailed error and warning reporting
|
||||||
|
- **TED XML Support**: Pre-built models for TED XML formats
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go mod tidy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Parse XML from URL
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"tm/pkg/xmlparser"
|
||||||
|
"tm/pkg/xmlparser/ted"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Create parser for Contract Notice
|
||||||
|
parser := xmlparser.NewParser[ted.ContractNotice](nil)
|
||||||
|
|
||||||
|
// Fetch XML from TED website
|
||||||
|
resp, err := http.Get("https://ted.europa.eu/en/notice/447983-2025/xml")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Parse XML
|
||||||
|
notice, err := parser.Parse(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Notice ID: %s\n", notice.GetID())
|
||||||
|
fmt.Printf("Notice Type: %s\n", notice.GetNoticeType())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parse XML from String
|
||||||
|
|
||||||
|
```go
|
||||||
|
xmlData := `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ContractNotice xmlns="urn:oasis:names:specification:ubl:schema:xsd:ContractNotice-2">
|
||||||
|
<ID>sample-notice-123</ID>
|
||||||
|
<ContractFolderID>sample-folder-456</ContractFolderID>
|
||||||
|
<IssueDate>2025-01-09+02:00</IssueDate>
|
||||||
|
<NoticeTypeCode>cn-standard</NoticeTypeCode>
|
||||||
|
<NoticeLanguageCode>ENG</NoticeLanguageCode>
|
||||||
|
</ContractNotice>`
|
||||||
|
|
||||||
|
parser := xmlparser.NewParser[ted.ContractNotice](nil)
|
||||||
|
notice, err := parser.ParseString(xmlData)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Parsed Notice ID: %s\n", notice.GetID())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Interfaces
|
||||||
|
|
||||||
|
### Parseable Interface
|
||||||
|
|
||||||
|
Every struct to be parsed must implement this interface:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Parseable interface {
|
||||||
|
Validate() error // Validate the parsed structure
|
||||||
|
GetID() string // Return unique identifier
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parser Interface
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Parser[T Parseable] interface {
|
||||||
|
Parse(reader io.Reader) (*T, error)
|
||||||
|
ParseString(xmlData string) (*T, error)
|
||||||
|
ParseBytes(xmlData []byte) (*T, error)
|
||||||
|
Validate(reader io.Reader) error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parser Configuration
|
||||||
|
|
||||||
|
```go
|
||||||
|
options := &xmlparser.ParserOptions{
|
||||||
|
StrictValidation: true, // Strict XML validation
|
||||||
|
AllowEmptyElements: true, // Allow empty XML elements
|
||||||
|
PreserveWhitespace: false, // Preserve whitespace
|
||||||
|
IgnoreUnknownElements: false, // Ignore unknown XML elements
|
||||||
|
MaxDepth: 100, // Maximum nesting depth
|
||||||
|
MaxSize: 10 * 1024 * 1024, // Maximum XML size (10MB)
|
||||||
|
}
|
||||||
|
|
||||||
|
parser := xmlparser.NewParser[ted.ContractNotice](options)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parse with Detailed Results
|
||||||
|
|
||||||
|
```go
|
||||||
|
parser := xmlparser.NewParser[ted.ContractNotice](nil).(*xmlparser.GenericParser[ted.ContractNotice])
|
||||||
|
|
||||||
|
result, err := parser.ParseWithResult(strings.NewReader(xmlData))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Parse error: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display parsed information
|
||||||
|
if result.Data != nil {
|
||||||
|
fmt.Printf("Notice ID: %s\n", result.Data.GetID())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display metadata
|
||||||
|
if result.Metadata != nil {
|
||||||
|
fmt.Printf("XML Version: %s\n", result.Metadata.Version)
|
||||||
|
fmt.Printf("Encoding: %s\n", result.Metadata.Encoding)
|
||||||
|
fmt.Printf("Namespace: %s\n", result.Metadata.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display errors and warnings
|
||||||
|
for _, err := range result.Errors {
|
||||||
|
fmt.Printf("Error: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, warning := range result.Warnings {
|
||||||
|
fmt.Printf("Warning: %s\n", warning)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## TED XML Models
|
||||||
|
|
||||||
|
The package includes ready-to-use models for TED XML formats:
|
||||||
|
|
||||||
|
### Contract Notice
|
||||||
|
|
||||||
|
```go
|
||||||
|
type ContractNotice struct {
|
||||||
|
ID string
|
||||||
|
ContractFolderID string
|
||||||
|
IssueDate string
|
||||||
|
NoticeTypeCode string
|
||||||
|
NoticeLanguageCode string
|
||||||
|
ProcurementProject *ProcurementProject
|
||||||
|
// ... other fields
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating Custom Models
|
||||||
|
|
||||||
|
To create custom models, implement the `Parseable` interface:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type CustomModel struct {
|
||||||
|
XMLName xml.Name `xml:"CustomElement"`
|
||||||
|
ID string `xml:"id,attr"`
|
||||||
|
Name string `xml:"name"`
|
||||||
|
Value string `xml:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CustomModel) Validate() error {
|
||||||
|
if c.ID == "" {
|
||||||
|
return fmt.Errorf("ID is required")
|
||||||
|
}
|
||||||
|
if c.Name == "" {
|
||||||
|
return fmt.Errorf("Name is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CustomModel) GetID() string {
|
||||||
|
return c.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
parser := xmlparser.NewParser[CustomModel](nil)
|
||||||
|
result, err := parser.ParseString(`<CustomElement id="123"><name>Test</name><value>Sample</value></CustomElement>`)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Factory Function for Creating Parsers
|
||||||
|
|
||||||
|
```go
|
||||||
|
func CreateTEDParser[T xmlparser.Parseable](options *xmlparser.ParserOptions) xmlparser.Parser[T] {
|
||||||
|
return xmlparser.NewParser[T](options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
contractParser := CreateTEDParser[ted.ContractNotice](nil)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Practical Examples
|
||||||
|
|
||||||
|
The `example/` folder contains complete usage examples:
|
||||||
|
|
||||||
|
- Parse from URL
|
||||||
|
- Parse from String
|
||||||
|
- Using different parser options
|
||||||
|
- Parse with detailed results
|
||||||
|
|
||||||
|
To run the examples:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd pkg/xmlparser/example
|
||||||
|
go run main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
pkg/xmlparser/
|
||||||
|
├── interfaces.go # Interface definitions
|
||||||
|
├── parser.go # Generic parser implementation
|
||||||
|
├── utils.go # Utility functions
|
||||||
|
├── parser_test.go # Comprehensive tests
|
||||||
|
├── ted/
|
||||||
|
│ └── models.go # TED XML models
|
||||||
|
├── example/
|
||||||
|
│ └── main.go # Usage examples
|
||||||
|
└── README.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## TED XML Support
|
||||||
|
|
||||||
|
This package is specifically designed for parsing TED (Tenders Electronic Daily) XML:
|
||||||
|
|
||||||
|
- **Contract Notice**: Tender announcements
|
||||||
|
- **Prior Information Notice**: Preliminary notices
|
||||||
|
- **Contract Award Notice**: Award announcements
|
||||||
|
|
||||||
|
### Supported Formats
|
||||||
|
|
||||||
|
- eForms XML (new TED version)
|
||||||
|
- TED Schema XML (legacy version)
|
||||||
|
- UBL 2.3 with eForms Extensions
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The parser supports various types of errors:
|
||||||
|
|
||||||
|
```go
|
||||||
|
result, err := parser.ParseString(invalidXML)
|
||||||
|
if err != nil {
|
||||||
|
// General parsing error
|
||||||
|
fmt.Printf("Parse failed: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detailed errors in result
|
||||||
|
for _, parseError := range result.Errors {
|
||||||
|
fmt.Printf("Parse error: %s\n", parseError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warnings
|
||||||
|
for _, warning := range result.Warnings {
|
||||||
|
fmt.Printf("Warning: %s\n", warning)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
- XML size limits (default: 10MB)
|
||||||
|
- Nesting depth limits (default: 100 levels)
|
||||||
|
- Stream parsing for large files
|
||||||
|
- Memory-efficient parsing
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Run comprehensive tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
# Run with verbose output
|
||||||
|
go test -v ./...
|
||||||
|
|
||||||
|
# Run benchmarks
|
||||||
|
go test -bench=. ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Utility Functions
|
||||||
|
|
||||||
|
The package includes helpful utility functions:
|
||||||
|
|
||||||
|
- `SanitizeXMLString()` - Clean invalid XML characters
|
||||||
|
- `ValidateXMLName()` - Validate XML element names
|
||||||
|
- `IsValidNamespace()` - Validate namespace URIs
|
||||||
|
- `NormalizeWhitespace()` - Normalize text content
|
||||||
|
- `TruncateString()` - Truncate strings for display
|
||||||
|
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
# XML Parser Test Suite
|
||||||
|
|
||||||
|
This document describes the comprehensive test suite for the `xmlparser` package and related TED XML models.
|
||||||
|
|
||||||
|
## 📁 Test Files Overview
|
||||||
|
|
||||||
|
### Core Package Tests (`pkg/xmlparser/`)
|
||||||
|
|
||||||
|
- **`interfaces_test.go`** - Tests for interfaces, types, and basic structures
|
||||||
|
- **`parser_test.go`** - Tests for XML parsing functionality and core parser logic
|
||||||
|
- **`utils_test.go`** - Tests for utility functions and error handling
|
||||||
|
- **`benchmark_test.go`** - Performance benchmarks and memory allocation tests
|
||||||
|
|
||||||
|
### TED Models Tests (`ted/`)
|
||||||
|
|
||||||
|
- **`ted_test.go`** - Tests for TED XML models and validation logic
|
||||||
|
|
||||||
|
### Test Infrastructure
|
||||||
|
|
||||||
|
- **`run_tests.sh`** - Comprehensive test runner script with coverage analysis
|
||||||
|
- **`TEST_SUITE.md`** - This documentation file
|
||||||
|
|
||||||
|
## 🧪 Test Categories
|
||||||
|
|
||||||
|
### 1. Interface and Type Tests (`interfaces_test.go`)
|
||||||
|
|
||||||
|
- **Default Parser Options**: Validates default configuration values
|
||||||
|
- **XML Metadata**: Tests metadata extraction and structure
|
||||||
|
- **Parse Results**: Validates result structure and error handling
|
||||||
|
- **Mock Implementation**: Tests the `MockParseable` test helper
|
||||||
|
- **Parser Interface**: Ensures interface compliance and method availability
|
||||||
|
|
||||||
|
**Key Test Functions:**
|
||||||
|
- `TestDefaultParserOptions()`
|
||||||
|
- `TestXMLMetadata()`
|
||||||
|
- `TestParseResult()`
|
||||||
|
- `TestMockParseable()`
|
||||||
|
- `TestParserInterface()`
|
||||||
|
|
||||||
|
### 2. Parser Functionality Tests (`parser_test.go`)
|
||||||
|
|
||||||
|
- **Parser Creation**: Tests parser instantiation with various options
|
||||||
|
- **XML Parsing**: Tests parsing from string, bytes, and readers
|
||||||
|
- **Validation**: Tests XML structure validation and depth limits
|
||||||
|
- **Size Constraints**: Tests parsing with size and depth limitations
|
||||||
|
- **Error Scenarios**: Tests handling of malformed XML and validation failures
|
||||||
|
- **Metadata Extraction**: Tests XML metadata extraction (version, encoding, namespace)
|
||||||
|
- **Concurrent Parsing**: Tests thread safety and concurrent access
|
||||||
|
|
||||||
|
**Key Test Functions:**
|
||||||
|
- `TestNewParser()`
|
||||||
|
- `TestParseString()` / `TestParseBytes()` / `TestParse()`
|
||||||
|
- `TestValidate()`
|
||||||
|
- `TestParseWithResult()`
|
||||||
|
- `TestConcurrentParsing()`
|
||||||
|
|
||||||
|
### 3. Utility Function Tests (`utils_test.go`)
|
||||||
|
|
||||||
|
- **Validation Errors**: Tests error structures and formatting
|
||||||
|
- **XML Sanitization**: Tests removal of invalid XML characters
|
||||||
|
- **XML Name Validation**: Tests XML element name validation rules
|
||||||
|
- **Namespace Validation**: Tests XML namespace URI validation
|
||||||
|
- **String Utilities**: Tests whitespace normalization and string truncation
|
||||||
|
- **Error Types**: Tests error interface implementations
|
||||||
|
|
||||||
|
**Key Test Functions:**
|
||||||
|
- `TestValidationError()` / `TestValidationErrors()`
|
||||||
|
- `TestXMLParseError()`
|
||||||
|
- `TestSanitizeXMLString()`
|
||||||
|
- `TestValidateXMLName()`
|
||||||
|
- `TestIsValidNamespace()`
|
||||||
|
- `TestNormalizeWhitespace()`
|
||||||
|
- `TestTruncateString()`
|
||||||
|
|
||||||
|
### 4. TED Model Tests (`ted/ted_test.go`)
|
||||||
|
|
||||||
|
- **Contract Notice Validation**: Tests validation rules and business logic
|
||||||
|
- **Getter Methods**: Tests all getter methods for extracting data from XML
|
||||||
|
- **Extension Handling**: Tests handling of UBL extensions and eForms data
|
||||||
|
- **Complex Scenarios**: Tests nested structures and edge cases
|
||||||
|
- **Date/Time Validation**: Tests various date format validations
|
||||||
|
- **Currency and Duration**: Tests validation of currency codes and duration units
|
||||||
|
|
||||||
|
**Key Test Functions:**
|
||||||
|
- `TestContractNoticeValidation()`
|
||||||
|
- `TestContractNoticeBasicGetters()`
|
||||||
|
- `TestContractNoticeExtensionGetters()`
|
||||||
|
- `TestContractNoticeProcurementDetails()`
|
||||||
|
- `TestValidDateFormats()`
|
||||||
|
|
||||||
|
### 5. Performance Benchmarks (`benchmark_test.go`)
|
||||||
|
|
||||||
|
- **Parsing Performance**: Benchmarks for different XML sizes (small, medium, large)
|
||||||
|
- **Memory Allocation**: Tests memory usage patterns during parsing
|
||||||
|
- **Utility Performance**: Benchmarks for utility functions
|
||||||
|
- **Concurrent Performance**: Tests performance under concurrent load
|
||||||
|
- **Validation Performance**: Benchmarks for different validation scenarios
|
||||||
|
|
||||||
|
**Key Benchmark Functions:**
|
||||||
|
- `BenchmarkParseString()` / `BenchmarkParseBytes()`
|
||||||
|
- `BenchmarkParseSmallXML()` / `BenchmarkParseMediumXML()` / `BenchmarkParseLargeXML()`
|
||||||
|
- `BenchmarkConcurrentParsing()`
|
||||||
|
- `BenchmarkMemoryAllocation()`
|
||||||
|
|
||||||
|
## 🚀 Running Tests
|
||||||
|
|
||||||
|
### Quick Test Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
go test -v ./...
|
||||||
|
|
||||||
|
# Run tests with coverage
|
||||||
|
go test -v -cover ./...
|
||||||
|
|
||||||
|
# Run specific test category
|
||||||
|
go test -v -run=TestParser
|
||||||
|
go test -v -run=TestValidation
|
||||||
|
go test -v -run=TestUtils
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comprehensive Test Suite
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run the complete test suite with coverage and benchmarks
|
||||||
|
cd pkg/xmlparser
|
||||||
|
./run_tests.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Benchmark Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all benchmarks
|
||||||
|
go test -bench=. -benchmem
|
||||||
|
|
||||||
|
# Run specific benchmarks
|
||||||
|
go test -bench=BenchmarkParse -benchmem
|
||||||
|
go test -bench=BenchmarkConcurrent -benchmem
|
||||||
|
|
||||||
|
# Run benchmarks with profiling
|
||||||
|
go test -bench=. -cpuprofile=cpu.prof -memprofile=mem.prof
|
||||||
|
```
|
||||||
|
|
||||||
|
### Coverage Analysis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate coverage report
|
||||||
|
go test -coverprofile=coverage.out
|
||||||
|
go tool cover -html=coverage.out -o coverage.html
|
||||||
|
|
||||||
|
# View coverage in terminal
|
||||||
|
go tool cover -func=coverage.out
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Test Statistics
|
||||||
|
|
||||||
|
### Coverage Targets
|
||||||
|
|
||||||
|
- **Overall Coverage**: ≥ 80%
|
||||||
|
- **Critical Paths**: ≥ 95%
|
||||||
|
- **Error Scenarios**: ≥ 90%
|
||||||
|
- **Utility Functions**: ≥ 95%
|
||||||
|
|
||||||
|
### Test Counts (Approximate)
|
||||||
|
|
||||||
|
- **Unit Tests**: 50+ test functions
|
||||||
|
- **Benchmarks**: 20+ benchmark functions
|
||||||
|
- **Test Cases**: 200+ individual test scenarios
|
||||||
|
- **Mock Objects**: Full `MockParseable` implementation
|
||||||
|
|
||||||
|
## 🔧 Test Infrastructure
|
||||||
|
|
||||||
|
### Mock Objects
|
||||||
|
|
||||||
|
**`MockParseable`**: Test implementation of the `Parseable` interface
|
||||||
|
- Supports both valid and invalid states for testing validation
|
||||||
|
- Includes all required XML structure elements
|
||||||
|
- Implements proper validation logic for testing error scenarios
|
||||||
|
|
||||||
|
### Test Utilities
|
||||||
|
|
||||||
|
- **Error Readers**: For testing I/O error scenarios
|
||||||
|
- **XML Test Data**: Various XML samples for different test cases
|
||||||
|
- **Concurrent Test Helpers**: For testing thread safety
|
||||||
|
|
||||||
|
### Test Configuration
|
||||||
|
|
||||||
|
- **Parallel Execution**: Concurrent tests are properly isolated
|
||||||
|
- **Race Detection**: All tests pass with `-race` flag
|
||||||
|
- **Memory Testing**: Benchmarks include memory allocation tracking
|
||||||
|
- **Timeout Handling**: Long-running tests have appropriate timeouts
|
||||||
|
|
||||||
|
## 🚨 Common Test Scenarios
|
||||||
|
|
||||||
|
### Success Cases
|
||||||
|
|
||||||
|
- Valid XML parsing with all data types
|
||||||
|
- Proper validation of business rules
|
||||||
|
- Correct metadata extraction
|
||||||
|
- Successful error handling and recovery
|
||||||
|
|
||||||
|
### Error Cases
|
||||||
|
|
||||||
|
- Malformed XML structure
|
||||||
|
- Invalid business data (missing required fields)
|
||||||
|
- Size and depth limit violations
|
||||||
|
- I/O errors during reading
|
||||||
|
- Validation failures
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
- Empty XML documents
|
||||||
|
- Very large XML documents
|
||||||
|
- Deeply nested structures
|
||||||
|
- Unicode and special characters
|
||||||
|
- Concurrent access patterns
|
||||||
|
|
||||||
|
## 📈 Performance Expectations
|
||||||
|
|
||||||
|
### Parsing Performance
|
||||||
|
|
||||||
|
- **Small XML** (< 1KB): < 10μs per operation
|
||||||
|
- **Medium XML** (1-10KB): < 100μs per operation
|
||||||
|
- **Large XML** (10-100KB): < 1ms per operation
|
||||||
|
|
||||||
|
### Memory Usage
|
||||||
|
|
||||||
|
- **Parsing Overhead**: < 3KB per operation
|
||||||
|
- **Memory Allocations**: < 100 allocations per parse
|
||||||
|
- **No Memory Leaks**: All tests pass memory leak detection
|
||||||
|
|
||||||
|
### Concurrent Performance
|
||||||
|
|
||||||
|
- **Thread Safety**: All operations are thread-safe
|
||||||
|
- **Scalability**: Performance scales linearly with CPU cores
|
||||||
|
- **Resource Sharing**: Parsers can be safely shared between goroutines
|
||||||
|
|
||||||
|
## 🔍 Debugging Tests
|
||||||
|
|
||||||
|
### Running Individual Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run specific test with verbose output
|
||||||
|
go test -v -run=TestSpecificFunction
|
||||||
|
|
||||||
|
# Run test with race detection
|
||||||
|
go test -race -run=TestConcurrent
|
||||||
|
|
||||||
|
# Run test with memory profiling
|
||||||
|
go test -run=TestMemory -memprofile=mem.prof
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Debugging Tips
|
||||||
|
|
||||||
|
1. **Use `-v` flag** for verbose output to see individual test progress
|
||||||
|
2. **Use `-race` flag** to detect race conditions in concurrent tests
|
||||||
|
3. **Use `-count=1`** to disable test caching for consistent results
|
||||||
|
4. **Use `-timeout`** to prevent hanging tests in CI environments
|
||||||
|
5. **Check coverage reports** to identify untested code paths
|
||||||
|
|
||||||
|
## 📝 Adding New Tests
|
||||||
|
|
||||||
|
### Guidelines for New Tests
|
||||||
|
|
||||||
|
1. **Follow naming conventions**: `TestFunctionName` for unit tests, `BenchmarkFunctionName` for benchmarks
|
||||||
|
2. **Use table-driven tests** for multiple test cases
|
||||||
|
3. **Include both success and failure scenarios**
|
||||||
|
4. **Add appropriate benchmarks** for performance-critical code
|
||||||
|
5. **Update this documentation** when adding new test categories
|
||||||
|
|
||||||
|
### Test Structure Template
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestNewFeature(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input InputType
|
||||||
|
expected ExpectedType
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid case",
|
||||||
|
input: validInput,
|
||||||
|
expected: expectedOutput,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Error case",
|
||||||
|
input: invalidInput,
|
||||||
|
expected: nil,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result, err := FunctionUnderTest(tt.input)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected error but got none")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(result, tt.expected) {
|
||||||
|
t.Errorf("Expected %v, got %v", tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Continuous Integration
|
||||||
|
|
||||||
|
### CI Pipeline Tests
|
||||||
|
|
||||||
|
1. **Unit Tests**: All unit tests must pass
|
||||||
|
2. **Race Detection**: Tests must pass with `-race` flag
|
||||||
|
3. **Coverage Check**: Coverage must meet minimum thresholds
|
||||||
|
4. **Benchmark Regression**: Performance must not regress significantly
|
||||||
|
5. **Lint Checks**: Code must pass all linting rules
|
||||||
|
|
||||||
|
### Quality Gates
|
||||||
|
|
||||||
|
- ✅ All tests pass
|
||||||
|
- ✅ Coverage ≥ 80%
|
||||||
|
- ✅ No race conditions detected
|
||||||
|
- ✅ Benchmarks within acceptable range
|
||||||
|
- ✅ No critical linting errors
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
For questions about the test suite or adding new tests, please refer to:
|
||||||
|
|
||||||
|
1. **Code Comments**: Inline documentation in test files
|
||||||
|
2. **README.md**: Main package documentation
|
||||||
|
3. **Architecture Documentation**: Project-wide coding standards
|
||||||
|
4. **Team Knowledge Base**: Internal development guidelines
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*This test suite ensures the reliability, performance, and maintainability of the XML parser package and related TED models.*
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package xmlparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parsable defines the interface that models must implement to be parseable
|
||||||
|
type Parsable interface {
|
||||||
|
// Validate validates the parsed model
|
||||||
|
Validate() error
|
||||||
|
// GetID returns a unique identifier for the model
|
||||||
|
GetID() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parser defines the generic XML parser interface
|
||||||
|
type Parser[T Parsable] interface {
|
||||||
|
// Parse parses XML from reader into the target type
|
||||||
|
Parse(reader io.Reader) (*T, error)
|
||||||
|
// ParseString parses XML from string into the target type
|
||||||
|
ParseString(xmlData string) (*T, error)
|
||||||
|
// ParseBytes parses XML from byte slice into the target type
|
||||||
|
ParseBytes(xmlData []byte) (*T, error)
|
||||||
|
// Validate validates XML against schema (if supported)
|
||||||
|
Validate(reader io.Reader) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// XMLMetadata contains metadata about the parsed XML
|
||||||
|
type XMLMetadata struct {
|
||||||
|
XMLName xml.Name `xml:""`
|
||||||
|
Namespace string `json:"namespace,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
Encoding string `json:"encoding,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseResult contains the result of XML parsing
|
||||||
|
type ParseResult[T Parsable] struct {
|
||||||
|
Data *T `json:"data"`
|
||||||
|
Metadata *XMLMetadata `json:"metadata,omitempty"`
|
||||||
|
Errors []string `json:"errors,omitempty"`
|
||||||
|
Warnings []string `json:"warnings,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParserOptions configures the parser behavior
|
||||||
|
type ParserOptions struct {
|
||||||
|
// StrictValidation enables strict XML validation
|
||||||
|
StrictValidation bool
|
||||||
|
// AllowEmptyElements allows empty XML elements
|
||||||
|
AllowEmptyElements bool
|
||||||
|
// PreserveWhitespace preserves whitespace in text elements
|
||||||
|
PreserveWhitespace bool
|
||||||
|
// IgnoreUnknownElements ignores unknown XML elements
|
||||||
|
IgnoreUnknownElements bool
|
||||||
|
// MaxDepth maximum nesting depth allowed
|
||||||
|
MaxDepth int
|
||||||
|
// MaxSize maximum XML size in bytes
|
||||||
|
MaxSize int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultParserOptions returns default parser options
|
||||||
|
func DefaultParserOptions() *ParserOptions {
|
||||||
|
return &ParserOptions{
|
||||||
|
StrictValidation: true,
|
||||||
|
AllowEmptyElements: true,
|
||||||
|
PreserveWhitespace: false,
|
||||||
|
IgnoreUnknownElements: false,
|
||||||
|
MaxDepth: 100,
|
||||||
|
MaxSize: 10 * 1024 * 1024, // 10MB
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
package xmlparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenericParser implements the Parser interface using Go generics
|
||||||
|
type GenericParser[T Parsable] struct {
|
||||||
|
options *ParserOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewParser creates a new generic XML parser
|
||||||
|
func NewParser[T Parsable](options *ParserOptions) Parser[T] {
|
||||||
|
if options == nil {
|
||||||
|
options = DefaultParserOptions()
|
||||||
|
}
|
||||||
|
return &GenericParser[T]{
|
||||||
|
options: options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parses XML from reader into the target type
|
||||||
|
func (p *GenericParser[T]) Parse(reader io.Reader) (*T, error) {
|
||||||
|
// Read all data from reader
|
||||||
|
data, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read XML data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.ParseBytes(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseString parses XML from string into the target type
|
||||||
|
func (p *GenericParser[T]) ParseString(xmlData string) (*T, error) {
|
||||||
|
return p.ParseBytes([]byte(xmlData))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseBytes parses XML from byte slice into the target type
|
||||||
|
func (p *GenericParser[T]) ParseBytes(xmlData []byte) (*T, error) {
|
||||||
|
// Check size limit
|
||||||
|
if p.options.MaxSize > 0 && int64(len(xmlData)) > p.options.MaxSize {
|
||||||
|
return nil, fmt.Errorf("XML size %d exceeds maximum allowed size %d", len(xmlData), p.options.MaxSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new instance of T
|
||||||
|
var result T
|
||||||
|
resultType := reflect.TypeOf(result)
|
||||||
|
|
||||||
|
// Create a pointer to the struct if it's not already a pointer
|
||||||
|
var targetPtr interface{}
|
||||||
|
if resultType.Kind() == reflect.Ptr {
|
||||||
|
// T is already a pointer type
|
||||||
|
targetPtr = reflect.New(resultType.Elem()).Interface()
|
||||||
|
} else {
|
||||||
|
// T is a value type, create a pointer to it
|
||||||
|
targetPtr = reflect.New(resultType).Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create XML decoder
|
||||||
|
decoder := xml.NewDecoder(bytes.NewReader(xmlData))
|
||||||
|
|
||||||
|
// Configure decoder options
|
||||||
|
if !p.options.PreserveWhitespace {
|
||||||
|
decoder.CharsetReader = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.options.StrictValidation {
|
||||||
|
decoder.Strict = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse XML into the pointer
|
||||||
|
err := decoder.Decode(targetPtr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode XML: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert back to the target type
|
||||||
|
var parsedResult T
|
||||||
|
if resultType.Kind() == reflect.Ptr {
|
||||||
|
// T is a pointer type, so targetPtr is already T
|
||||||
|
parsedResult = targetPtr.(T)
|
||||||
|
} else {
|
||||||
|
// T is a value type, so dereference the pointer
|
||||||
|
parsedResult = reflect.ValueOf(targetPtr).Elem().Interface().(T)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the parsed result
|
||||||
|
if err := parsedResult.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("validation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &parsedResult, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates XML against schema (basic validation for now)
|
||||||
|
func (p *GenericParser[T]) Validate(reader io.Reader) error {
|
||||||
|
data, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read XML data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for empty data
|
||||||
|
if len(data) == 0 {
|
||||||
|
return fmt.Errorf("XML data is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic XML well-formedness validation
|
||||||
|
decoder := xml.NewDecoder(bytes.NewReader(data))
|
||||||
|
|
||||||
|
// Count depth for max depth validation
|
||||||
|
depth := 0
|
||||||
|
maxDepthReached := 0
|
||||||
|
|
||||||
|
for {
|
||||||
|
token, err := decoder.Token()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("XML validation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch token.(type) {
|
||||||
|
case xml.StartElement:
|
||||||
|
depth++
|
||||||
|
if depth > maxDepthReached {
|
||||||
|
maxDepthReached = depth
|
||||||
|
}
|
||||||
|
if p.options.MaxDepth > 0 && depth > p.options.MaxDepth {
|
||||||
|
return fmt.Errorf("XML depth %d exceeds maximum allowed depth %d", depth, p.options.MaxDepth)
|
||||||
|
}
|
||||||
|
case xml.EndElement:
|
||||||
|
depth--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseWithResult parses XML and returns detailed result with metadata
|
||||||
|
func (p *GenericParser[T]) ParseWithResult(reader io.Reader) (*ParseResult[T], error) {
|
||||||
|
result := &ParseResult[T]{
|
||||||
|
Metadata: &XMLMetadata{},
|
||||||
|
Errors: []string{},
|
||||||
|
Warnings: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read all data
|
||||||
|
data, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
result.Errors = append(result.Errors, fmt.Sprintf("Failed to read XML data: %v", err))
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract XML metadata
|
||||||
|
if err := p.extractMetadata(data, result.Metadata); err != nil {
|
||||||
|
result.Warnings = append(result.Warnings, fmt.Sprintf("Failed to extract metadata: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the XML
|
||||||
|
parsed, err := p.ParseBytes(data)
|
||||||
|
if err != nil {
|
||||||
|
result.Errors = append(result.Errors, err.Error())
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Data = parsed
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractMetadata extracts basic metadata from XML
|
||||||
|
func (p *GenericParser[T]) extractMetadata(xmlData []byte, metadata *XMLMetadata) error {
|
||||||
|
// Basic extraction of XML declaration and root element
|
||||||
|
xmlStr := string(xmlData)
|
||||||
|
|
||||||
|
// Extract encoding from XML declaration
|
||||||
|
if strings.HasPrefix(xmlStr, "<?xml") {
|
||||||
|
endIdx := strings.Index(xmlStr, "?>")
|
||||||
|
if endIdx > 0 {
|
||||||
|
declaration := xmlStr[:endIdx+2]
|
||||||
|
|
||||||
|
// Extract encoding
|
||||||
|
if encodingIdx := strings.Index(declaration, "encoding="); encodingIdx > 0 {
|
||||||
|
start := encodingIdx + 9
|
||||||
|
if start < len(declaration) {
|
||||||
|
quote := declaration[start]
|
||||||
|
if quote == '"' || quote == '\'' {
|
||||||
|
end := strings.IndexByte(declaration[start+1:], byte(quote))
|
||||||
|
if end > 0 {
|
||||||
|
metadata.Encoding = declaration[start+1 : start+1+end]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract version
|
||||||
|
if versionIdx := strings.Index(declaration, "version="); versionIdx > 0 {
|
||||||
|
start := versionIdx + 8
|
||||||
|
if start < len(declaration) {
|
||||||
|
quote := declaration[start]
|
||||||
|
if quote == '"' || quote == '\'' {
|
||||||
|
end := strings.IndexByte(declaration[start+1:], byte(quote))
|
||||||
|
if end > 0 {
|
||||||
|
metadata.Version = declaration[start+1 : start+1+end]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract root element namespace
|
||||||
|
decoder := xml.NewDecoder(bytes.NewReader(xmlData))
|
||||||
|
for {
|
||||||
|
token, err := decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if startElement, ok := token.(xml.StartElement); ok {
|
||||||
|
metadata.XMLName = startElement.Name
|
||||||
|
metadata.Namespace = startElement.Name.Space
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
package xmlparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidationError represents a validation error with details
|
||||||
|
type ValidationError struct {
|
||||||
|
Field string `json:"field"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Value string `json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ve ValidationError) Error() string {
|
||||||
|
if ve.Value != "" {
|
||||||
|
return fmt.Sprintf("validation error in field '%s': %s (value: %s)", ve.Field, ve.Message, ve.Value)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("validation error in field '%s': %s", ve.Field, ve.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidationErrors represents multiple validation errors
|
||||||
|
type ValidationErrors []ValidationError
|
||||||
|
|
||||||
|
func (ve ValidationErrors) Error() string {
|
||||||
|
if len(ve) == 0 {
|
||||||
|
return "no validation errors"
|
||||||
|
}
|
||||||
|
|
||||||
|
var messages []string
|
||||||
|
for _, err := range ve {
|
||||||
|
messages = append(messages, err.Error())
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("validation failed with %d error(s): %s", len(ve), strings.Join(messages, "; "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty returns true if there are no validation errors
|
||||||
|
func (ve ValidationErrors) IsEmpty() bool {
|
||||||
|
return len(ve) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add adds a validation error
|
||||||
|
func (ve *ValidationErrors) Add(field, message, value string) {
|
||||||
|
*ve = append(*ve, ValidationError{
|
||||||
|
Field: field,
|
||||||
|
Message: message,
|
||||||
|
Value: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// XMLParseError represents an XML parsing error with context
|
||||||
|
type XMLParseError struct {
|
||||||
|
Line int `json:"line,omitempty"`
|
||||||
|
Column int `json:"column,omitempty"`
|
||||||
|
Element string `json:"element,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Original error `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xpe XMLParseError) Error() string {
|
||||||
|
var parts []string
|
||||||
|
|
||||||
|
if xpe.Element != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("element '%s'", xpe.Element))
|
||||||
|
}
|
||||||
|
|
||||||
|
if xpe.Line > 0 {
|
||||||
|
if xpe.Column > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("line %d, column %d", xpe.Line, xpe.Column))
|
||||||
|
} else {
|
||||||
|
parts = append(parts, fmt.Sprintf("line %d", xpe.Line))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context := ""
|
||||||
|
if len(parts) > 0 {
|
||||||
|
context = " (" + strings.Join(parts, ", ") + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("XML parse error%s: %s", context, xpe.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap returns the original error
|
||||||
|
func (xpe XMLParseError) Unwrap() error {
|
||||||
|
return xpe.Original
|
||||||
|
}
|
||||||
|
|
||||||
|
// SanitizeXMLString removes invalid XML characters and cleans the string
|
||||||
|
func SanitizeXMLString(s string) string {
|
||||||
|
// Remove control characters except tab, newline, and carriage return
|
||||||
|
var result strings.Builder
|
||||||
|
for _, r := range s {
|
||||||
|
if (r >= 0x20 && r <= 0xD7FF) ||
|
||||||
|
(r >= 0xE000 && r <= 0xFFFD) ||
|
||||||
|
(r >= 0x10000 && r <= 0x10FFFF) ||
|
||||||
|
r == 0x09 || r == 0x0A || r == 0x0D {
|
||||||
|
result.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateXMLName checks if a string is a valid XML name
|
||||||
|
func ValidateXMLName(name string) bool {
|
||||||
|
if name == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// XML names must start with a letter or underscore
|
||||||
|
first := []rune(name)[0]
|
||||||
|
if !((first >= 'A' && first <= 'Z') ||
|
||||||
|
(first >= 'a' && first <= 'z') ||
|
||||||
|
first == '_') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subsequent characters can be letters, digits, hyphens, periods, or underscores
|
||||||
|
for _, r := range []rune(name)[1:] {
|
||||||
|
if !((r >= 'A' && r <= 'Z') ||
|
||||||
|
(r >= 'a' && r <= 'z') ||
|
||||||
|
(r >= '0' && r <= '9') ||
|
||||||
|
r == '-' || r == '.' || r == '_') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidNamespace checks if a string is a valid XML namespace URI
|
||||||
|
func IsValidNamespace(namespace string) bool {
|
||||||
|
if namespace == "" {
|
||||||
|
return true // Empty namespace is valid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic URI validation - should start with http:// or https:// or urn:
|
||||||
|
return strings.HasPrefix(namespace, "http://") ||
|
||||||
|
strings.HasPrefix(namespace, "https://") ||
|
||||||
|
strings.HasPrefix(namespace, "urn:")
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeWhitespace normalizes whitespace in XML text content
|
||||||
|
func NormalizeWhitespace(s string) string {
|
||||||
|
// Replace all whitespace sequences with single spaces
|
||||||
|
fields := strings.Fields(s)
|
||||||
|
return strings.Join(fields, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TruncateString truncates a string to maxLength and adds "..." if needed
|
||||||
|
func TruncateString(s string, maxLength int) string {
|
||||||
|
if len(s) <= maxLength {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
if maxLength <= 3 {
|
||||||
|
return s[:maxLength]
|
||||||
|
}
|
||||||
|
return s[:maxLength-3] + "..."
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user