Remove hello world endpoints and refactor routing to use new hello package

- Deleted the existing hello world handlers from hello.go to streamline the codebase.
- Updated openapi.go to attach the new hello package for handling hello world routes, ensuring OAuth protection is applied correctly.
- Enhanced the README.md to reflect the new structure and provide comprehensive documentation for the hello world API endpoints.
This commit is contained in:
Max 2025-07-22 16:55:24 +08:00
parent d54572a7d1
commit 92d21c5389
9 changed files with 2539 additions and 18 deletions

107
openapi/COMMERCIAL.md Normal file
View file

@ -0,0 +1,107 @@
# Commercial Usage Declaration - Yao OpenAPI
This document outlines the commercial usage terms for the Yao OpenAPI components contained within this directory.
## Commercial License Requirements
The Yao OpenAPI module is available for commercial use under the following **mandatory conditions**:
### 1. Developer Certificate Requirement
**Commercial usage requires obtaining a Yao Developer Certificate.**
- All commercial deployments of Yao OpenAPI must possess a valid Yao Developer Certificate
- The certificate must be obtained through official Yao certification channels
- Usage without proper certification constitutes license violation
### 2. Trademark and Branding Restrictions
**Yao branding elements are protected and must remain intact:**
- Yao App Engine logos and copyright information **MUST NOT** be removed or modified
- All Yao trademark elements must remain visible and unaltered in production deployments
- Logo and copyright modifications are **ONLY** permitted with explicit authorization through Yao Developer Certificate
### 3. Certificate Verification System Integrity
**The Yao certificate verification system must be maintained in its entirety:**
- **PROHIBITED**: Removal, modification, or circumvention of Yao certificate verification logic
- **PROHIBITED**: Alteration of certificate validation processes and related code components
- **PROHIBITED**: Modification of certificate verification pages (marked in code comments)
- **REQUIRED**: Complete preservation of the Yao certificate verification system regardless of usage purpose
### 4. Code Integrity Requirements
For commercial deployments, the following **core verification mechanisms** must remain unmodified:
- Certificate validation endpoints and middleware
- Authentication verification logic and OAuth implementation
- License verification routines and related authentication flows
- Commercial license checking and enforcement code
- Yao branding and copyright verification systems
_Note: General business logic and API customizations are permitted, but the core licensing and certificate verification infrastructure must remain intact._
## Compliance Obligations
### For Commercial Users:
1. **Obtain Valid Certification**: Acquire and maintain a current Yao Developer Certificate
2. **Preserve Verification Logic**: Maintain all certificate verification code intact
3. **Respect Trademark Rights**: Keep all Yao branding elements unmodified
4. **Regular Compliance Review**: Ensure ongoing adherence to certification requirements
### For Developers and Integrators:
1. **Code Review Responsibility**: Verify that certificate verification logic remains intact
2. **Documentation Compliance**: Maintain all certificate-related documentation and comments
3. **Deployment Verification**: Ensure production deployments include proper certificate validation
4. **Update Compliance**: When updating Yao components, preserve all certificate verification systems
## Prohibited Activities
The following activities are **strictly prohibited** without explicit written authorization:
- Removing or disabling certificate verification mechanisms
- Modifying Yao branding, logos, or copyright notices
- Circumventing license validation processes
- Redistributing modified versions that bypass certificate requirements
- Creating derivative works that remove authentication restrictions
## Legal Compliance
This commercial usage declaration is governed by:
- **Base License**: Modified Apache License 2.0
- **Additional Terms**: As specified in the root LICENSE file
- **Commercial Requirements**: As outlined in this document
- **Jurisdiction**: Subject to applicable intellectual property and software licensing laws
## Violation Consequences
Failure to comply with these commercial usage terms may result in:
- Immediate license termination
- Legal action for intellectual property infringement
- Liability for damages and legal costs
- Permanent prohibition from commercial Yao ecosystem usage
## Contact and Authorization
For commercial licensing inquiries, certificate applications, or clarification of usage terms:
- **Official Website**: [Yao Developer Certification Portal]
- **License Inquiries**: Contact authorized Yao licensing representatives
- **Technical Support**: Available exclusively to certified commercial users
---
**Important Notice**: This commercial declaration supplements and does not replace the terms specified in the root LICENSE file. In case of conflicts, the most restrictive terms apply.
**© 2025 Infinite Wisdom Software. All rights reserved.**
---
_Last Updated: 2025-07-22_
_Document Version: 1.0_

107
openapi/COMMERCIAL.zh-CN.md Normal file
View file

@ -0,0 +1,107 @@
# 商业使用声明 - Yao OpenAPI
本文档阐述了本目录下 Yao OpenAPI 组件的商业使用条款。
## 商业许可证要求
Yao OpenAPI 模块可用于商业用途,但须遵循以下**强制性条件**
### 1. 开发者证书要求
**商业使用需要获得 Yao 开发者证书。**
- 所有 Yao OpenAPI 的商业部署都必须拥有有效的 Yao 开发者证书
- 证书必须通过官方 Yao 认证渠道获得
- 未经适当认证的使用构成许可证违规
### 2. 商标和品牌限制
**Yao 品牌元素受保护,必须保持完整:**
- Yao App Engine 标志和版权信息**不得**被移除或修改
- 所有 Yao 商标元素必须在生产部署中保持可见且未改动
- 标志和版权修改**仅**在通过 Yao 开发者证书获得明确授权后允许
### 3. 证书验证系统完整性
**Yao 证书验证系统必须完整保持:**
- **禁止**:移除、修改或绕过 Yao 证书验证逻辑
- **禁止**:更改证书验证流程和相关代码组件
- **禁止**:修改证书验证页面(在代码注释中标记)
- **要求**:无论使用目的如何,必须完整保持 Yao 证书验证系统
### 4. 代码完整性要求
对于商业部署,以下**核心验证机制**必须保持未修改状态:
- 证书验证端点和中间件
- 认证验证逻辑和 OAuth 实现
- 许可证验证例程和相关认证流程
- 商业许可证检查和执行代码
- Yao 品牌和版权验证系统
_注意一般业务逻辑和 API 定制是允许的但核心许可证和证书验证基础设施必须保持完整。_
## 合规义务
### 商业用户:
1. **获得有效认证**:获得并维护当前有效的 Yao 开发者证书
2. **保持验证逻辑**:保持所有证书验证代码完整
3. **尊重商标权**:保持所有 Yao 品牌元素未修改
4. **定期合规审查**:确保持续遵守认证要求
### 开发者和集成商:
1. **代码审查责任**:验证证书验证逻辑保持完整
2. **文档合规**:维护所有与证书相关的文档和注释
3. **部署验证**:确保生产部署包含适当的证书验证
4. **更新合规**:更新 Yao 组件时,保持所有证书验证系统
## 禁止活动
以下活动在没有明确书面授权的情况下**严格禁止**
- 移除或禁用证书验证机制
- 修改 Yao 品牌、标志或版权声明
- 绕过许可证验证流程
- 重新分发绕过证书要求的修改版本
- 创建移除认证限制的衍生作品
## 法律合规
本商业使用声明受以下条款管辖:
- **基础许可证**:修改版 Apache License 2.0
- **附加条款**:如根目录 LICENSE 文件中指定
- **商业要求**:如本文档中概述
- **管辖权**:受适用的知识产权和软件许可法律约束
## 违规后果
未能遵守这些商业使用条款可能导致:
- 立即终止许可证
- 因知识产权侵权而提起法律诉讼
- 损害赔偿和法律费用责任
- 永久禁止商业 Yao 生态系统使用
## 联系和授权
如需商业许可咨询、证书申请或使用条款澄清:
- **官方网站**[Yao 开发者认证门户]
- **许可证咨询**:联系授权的 Yao 许可代表
- **技术支持**:仅向经认证的商业用户提供
---
**重要声明**:本商业声明是对根目录 LICENSE 文件中指定条款的补充,不能替代这些条款。如有冲突,以最严格的条款为准。
**© 2025 Infinite Wisdom Software. 保留所有权利。**
---
_最后更新2025-07-22_
_文档版本1.0_

View file

@ -1,9 +1,400 @@
# Yao OpenAPI
## OAuth
The Yao OpenAPI provides a comprehensive set of RESTful APIs for managing Yao applications, including OAuth 2.1/OpenID Connect authentication, DSL resource management, and development utilities.
## Agent
## Base URL
## Knowledge Base
All API endpoints are prefixed with a configurable base URL (e.g., `/v1`).
## Audit Log
## Authentication
The Yao OpenAPI implements OAuth 2.1 and OpenID Connect Core 1.0 specifications for secure authentication and authorization.
### Supported Grant Types
- **Authorization Code Flow** - RFC 6749 (recommended for web applications)
- **Client Credentials Flow** - RFC 6749 (for server-to-server communication)
- **Device Authorization Flow** - RFC 8628 (for devices with limited input)
- **Refresh Token Flow** - RFC 6749 (for token renewal)
- **Token Exchange** - RFC 8693 (for token delegation)
### Discovery Endpoints
The OpenAPI server provides standard OAuth 2.1 discovery endpoints:
```
GET /.well-known/oauth-authorization-server
```
Returns server metadata including supported endpoints, grant types, and security features.
### OAuth Endpoints
#### Authorization Endpoint
Initiate the authorization code flow:
```
GET /oauth/authorize?client_id={client_id}&response_type=code&redirect_uri={redirect_uri}&scope={scope}&state={state}
```
**Parameters:**
- `client_id` (required): Client identifier
- `response_type` (required): Must be "code"
- `redirect_uri` (required): Client redirect URI
- `scope` (optional): Requested scopes
- `state` (recommended): CSRF protection state parameter
- `code_challenge` (optional): PKCE code challenge
- `code_challenge_method` (optional): PKCE challenge method
#### Token Endpoint
Exchange authorization code for access token:
```
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code={code}&redirect_uri={redirect_uri}&client_id={client_id}&client_secret={client_secret}
```
**Client Credentials Flow:**
```
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id={client_id}&client_secret={client_secret}&scope={scope}
```
**Response:**
```json
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "def50200...",
"scope": "openid profile"
}
```
#### Token Introspection
Validate and inspect access tokens (RFC 7662):
```
POST /oauth/introspect
Content-Type: application/x-www-form-urlencoded
Authorization: Basic {base64(client_id:client_secret)}
token={access_token}
```
**Response:**
```json
{
"active": true,
"scope": "openid profile",
"client_id": "your_client_id",
"username": "user@example.com",
"token_type": "Bearer",
"exp": 1640995200,
"iat": 1640991600
}
```
#### Token Revocation
Revoke access or refresh tokens (RFC 7009):
```
POST /oauth/revoke
Content-Type: application/x-www-form-urlencoded
Authorization: Basic {base64(client_id:client_secret)}
token={token}&token_type_hint={access_token|refresh_token}
```
#### Dynamic Client Registration
Register OAuth clients dynamically (RFC 7591):
```
POST /oauth/register
Content-Type: application/json
{
"client_name": "My Application",
"redirect_uris": ["https://app.example.com/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "openid profile"
}
```
**Response:**
```json
{
"client_id": "generated_client_id",
"client_secret": "generated_client_secret",
"client_name": "My Application",
"redirect_uris": ["https://app.example.com/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "openid profile"
}
```
#### JSON Web Key Set
Retrieve public keys for token verification (RFC 7517):
```
GET /oauth/jwks
```
**Response:**
```json
{
"keys": [
{
"kty": "RSA",
"kid": "key-id-1",
"use": "sig",
"n": "...",
"e": "AQAB"
}
]
}
```
### Authentication Usage
#### Bearer Token Authentication
Include the access token in API requests:
```bash
curl -X GET "/v1/dsl/list/model" \
-H "Authorization: Bearer {access_token}"
```
#### Client Credentials
For server-to-server authentication, use the client credentials flow to obtain an access token, then include it in subsequent API requests.
## Hello World API
Simple endpoints for testing connectivity and authentication.
### Public Endpoint
Test basic connectivity without authentication:
```
GET /helloworld/public
POST /helloworld/public
```
**Response:**
```json
{
"MESSAGE": "HELLO, WORLD",
"SERVER_TIME": "2024-01-15T10:30:00Z",
"VERSION": "1.0.0",
"PRVERSION": "1.0.0-preview",
"CUI": "1.0.0",
"PRCUI": "1.0.0-preview",
"APP": "YaoApp",
"APP_VERSION": "1.0.0"
}
```
### Protected Endpoint
Test OAuth authentication:
```
GET /helloworld/protected
POST /helloworld/protected
```
**Headers:**
```
Authorization: Bearer {access_token}
```
**Response:**
Same as public endpoint, but requires valid authentication.
**Example:**
```bash
# Get access token first
curl -X POST "/v1/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=your_client&client_secret=your_secret"
# Use token to access protected endpoint
curl -X GET "/v1/helloworld/protected" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
```
## DSL Management API
Comprehensive API for managing Yao DSL resources (models, connectors, MCP clients, etc.).
**[View Full DSL API Documentation →](dsl/README.md)**
The DSL Management API provides:
- **Resource Management**: Create, read, update, delete DSL resources
- **Load Management**: Load, unload, reload DSL resources
- **Validation**: Validate DSL source code syntax
- **Execution**: Execute methods on loaded DSL resources
- **Discovery**: List and inspect available DSL resources
**Key Endpoints:**
- `GET /dsl/list/{type}` - List DSL resources
- `POST /dsl/create/{type}` - Create new DSL resource
- `GET /dsl/inspect/{type}/{id}` - Inspect DSL resource details
- `PUT /dsl/update/{type}` - Update existing DSL resource
- `DELETE /dsl/delete/{type}/{id}` - Delete DSL resource
All DSL endpoints require OAuth authentication.
## Error Responses
All endpoints return standardized error responses:
```json
{
"error": "invalid_request",
"error_description": "The request is missing a required parameter"
}
```
**Common HTTP Status Codes:**
- `200` - Success
- `201` - Created
- `400` - Bad Request (invalid parameters)
- `401` - Unauthorized (authentication required)
- `403` - Forbidden (insufficient permissions)
- `404` - Not Found
- `500` - Internal Server Error
**OAuth Error Codes:**
- `invalid_request` - Request is malformed
- `invalid_client` - Client authentication failed
- `invalid_grant` - Grant is invalid or expired
- `unauthorized_client` - Client not authorized for grant type
- `unsupported_grant_type` - Grant type not supported
- `invalid_scope` - Requested scope is invalid
## Security Features
The OpenAPI implements comprehensive security measures:
### OAuth 2.1 Security
- **PKCE (Proof Key for Code Exchange)** - Required for public clients
- **State Parameter** - CSRF protection for authorization requests
- **Secure Token Storage** - Access tokens with appropriate expiration
- **Client Authentication** - Multiple authentication methods supported
### HTTP Security Headers
All responses include security headers:
- `Cache-Control: no-store, no-cache, must-revalidate`
- `Pragma: no-cache`
- `X-Content-Type-Options: nosniff`
- `X-Frame-Options: DENY`
### Rate Limiting
API endpoints are protected against abuse with configurable rate limiting.
## Example Workflows
### Web Application Authentication
1. **Register your client** (if using dynamic registration):
```bash
curl -X POST "/v1/oauth/register" \
-H "Content-Type: application/json" \
-d '{
"client_name": "My Web App",
"redirect_uris": ["https://myapp.com/callback"],
"grant_types": ["authorization_code", "refresh_token"]
}'
```
2. **Initiate authorization flow**:
```
https://api.example.com/v1/oauth/authorize?client_id=your_client_id&response_type=code&redirect_uri=https://myapp.com/callback&scope=openid+profile&state=random_state
```
3. **Exchange authorization code for tokens**:
```bash
curl -X POST "/v1/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code&code=auth_code&redirect_uri=https://myapp.com/callback&client_id=your_client_id&client_secret=your_secret"
```
4. **Use access token to call APIs**:
```bash
curl -X GET "/v1/dsl/list/model" \
-H "Authorization: Bearer access_token_here"
```
### Server-to-Server Integration
1. **Obtain client credentials token**:
```bash
curl -X POST "/v1/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=server_client&client_secret=server_secret&scope=dsl:manage"
```
2. **Manage DSL resources**:
```bash
# Create a new model
curl -X POST "/v1/dsl/create/model" \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{
"id": "product",
"source": "{ \"name\": \"product\", \"table\": { \"name\": \"products\" }, \"columns\": [...] }"
}'
```
## Configuration
The OpenAPI server is configured through `openapi/openapi.yao`.
**[View Complete Configuration Examples →](https://github.com/YaoApp/yao-dev-app/tree/main/openapi)**
This includes comprehensive configuration examples for:
- OAuth 2.1 server settings
- Client registration and management
- Security and authentication policies
- Development and production environments
- API endpoint configuration

490
openapi/dsl/README.md Normal file
View file

@ -0,0 +1,490 @@
# DSL Management API
This document describes the RESTful API for managing Yao DSL resources (models, connectors, MCP clients, etc.).
## Base URL
All endpoints are prefixed with the configured base URL followed by `/dsl` (e.g., `/v1/dsl`).
## Authentication
All endpoints require OAuth authentication via the configured OAuth provider.
## DSL Types
Supported DSL types:
- `model` - Database models
- `connector` - External service connectors
- `mcp-client` - MCP client configurations
- `api` - HTTP API definitions
## Endpoints
### Information Endpoints
#### Inspect DSL
Get detailed information about a DSL resource.
```
GET /inspect/{type}/{id}
```
**Parameters:**
- `type` (path): DSL type
- `id` (path): DSL identifier
**Example:**
```bash
curl -X GET "/v1/dsl/inspect/model/user" \
-H "Authorization: Bearer {token}"
```
**Response:**
```json
{
"id": "user",
"type": "model",
"label": "User Model",
"description": "User management model",
"tags": ["auth", "user"],
"path": "models/user.mod.yao",
"store": "file",
"status": "loaded",
"readonly": false,
"builtin": false,
"mtime": "2024-01-15T10:30:00Z",
"ctime": "2024-01-10T09:00:00Z"
}
```
#### Get DSL Source Code
Retrieve the source code of a DSL resource.
```
GET /source/{type}/{id}
```
**Example:**
```bash
curl -X GET "/v1/dsl/source/model/user" \
-H "Authorization: Bearer {token}"
```
**Response:**
```json
{
"source": "{\n \"name\": \"user\",\n \"table\": {\n \"name\": \"users\"\n },\n \"columns\": [...]\n}"
}
```
#### Get DSL File Path
Get the file system path for a DSL resource.
```
GET /path/{type}/{id}
```
**Response:**
```json
{
"path": "models/user.mod.yao"
}
```
#### List DSLs
List DSL resources with optional filtering.
```
GET /list/{type}?sort={sort}&order={order}&store={store}&source={source}&tags={tags}&pattern={pattern}
```
**Query Parameters:**
- `sort` (optional): Sort field
- `order` (optional): Sort order ("asc" or "desc")
- `store` (optional): Storage type filter ("db" or "file")
- `source` (optional): Include source code in response (true/false)
- `tags` (optional): Comma-separated list of tags to filter by
- `pattern` (optional): File name pattern matching
**Example:**
```bash
curl -X GET "/v1/dsl/list/model?store=file&tags=user,auth" \
-H "Authorization: Bearer {token}"
```
**Response:**
```json
[
{
"id": "user",
"type": "model",
"label": "User Model",
"description": "User management model",
"tags": ["auth", "user"],
"path": "models/user.mod.yao",
"store": "file",
"status": "loaded"
}
]
```
#### Check DSL Existence
Check if a DSL resource exists.
```
GET /exists/{type}/{id}
```
**Response:**
```json
{
"exists": true
}
```
### CRUD Operations
#### Create DSL
Create a new DSL resource.
```
POST /create/{type}
```
**Request Body:**
```json
{
"id": "test_user",
"source": "{\n \"name\": \"test_user\",\n \"table\": {\n \"name\": \"test_users\",\n \"comment\": \"Test User\"\n },\n \"columns\": [\n { \"name\": \"id\", \"type\": \"ID\" },\n { \"name\": \"name\", \"type\": \"string\", \"length\": 80 }\n ]\n}",
"store": "file"
}
```
**Example:**
```bash
curl -X POST "/v1/dsl/create/model" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"id": "test_user",
"source": "{ \"name\": \"test_user\", \"table\": { \"name\": \"test_users\" }, \"columns\": [{ \"name\": \"id\", \"type\": \"ID\" }] }",
"store": "file"
}'
```
**Response:**
```json
{
"message": "DSL created successfully"
}
```
#### Update DSL
Update an existing DSL resource.
```
PUT /update/{type}
```
**Request Body:**
```json
{
"id": "test_user",
"source": "{\n \"name\": \"test_user\",\n \"table\": {\n \"name\": \"test_users\",\n \"comment\": \"Updated Test User\"\n },\n \"columns\": [\n { \"name\": \"id\", \"type\": \"ID\" },\n { \"name\": \"name\", \"type\": \"string\", \"length\": 100 }\n ]\n}"
}
```
**Response:**
```json
{
"message": "DSL updated successfully"
}
```
#### Delete DSL
Delete a DSL resource.
```
DELETE /delete/{type}/{id}
```
**Example:**
```bash
curl -X DELETE "/v1/dsl/delete/model/test_user" \
-H "Authorization: Bearer {token}"
```
**Response:**
```json
{
"message": "DSL deleted successfully"
}
```
### Load Management
#### Load DSL
Load a DSL resource into memory.
```
POST /load/{type}
```
**Request Body:**
```json
{
"id": "user",
"source": "{ ... }",
"store": "file"
}
```
**Response:**
```json
{
"message": "DSL loaded successfully"
}
```
#### Unload DSL
Unload a DSL resource from memory.
```
POST /unload/{type}
```
**Request Body:**
```json
{
"id": "user",
"store": "file"
}
```
**Response:**
```json
{
"message": "DSL unloaded successfully"
}
```
#### Reload DSL
Reload a DSL resource (unload then load).
```
POST /reload/{type}
```
**Request Body:**
```json
{
"id": "user",
"source": "{ ... }",
"store": "file"
}
```
**Response:**
```json
{
"message": "DSL reloaded successfully"
}
```
### Execution and Validation
#### Execute DSL Method
Execute a method on a loaded DSL resource.
```
POST /execute/{type}/{id}/{method}
```
**Request Body:**
```json
{
"args": ["arg1", "arg2"]
}
```
**Example:**
```bash
curl -X POST "/v1/dsl/execute/model/user/find" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"args": [1, {"select": ["id", "name"]}]
}'
```
**Response:**
```json
{
"result": {
"id": 1,
"name": "John Doe"
}
}
```
#### Validate DSL Source
Validate DSL source code syntax.
```
POST /validate/{type}
```
**Request Body:**
```json
{
"source": "{\n \"name\": \"user\",\n \"table\": {\n \"name\": \"users\"\n }\n}"
}
```
**Response:**
```json
{
"valid": true,
"messages": []
}
```
Or if there are validation errors:
```json
{
"valid": false,
"messages": [
{
"file": "",
"line": 5,
"column": 10,
"message": "Missing required field 'columns'",
"severity": "error"
}
]
}
```
## Error Responses
All endpoints return appropriate HTTP status codes and error messages:
```json
{
"error": "DSL ID is required"
}
```
Common HTTP status codes:
- `200` - Success
- `201` - Created
- `400` - Bad Request (invalid parameters)
- `404` - Not Found
- `500` - Internal Server Error
## Example Workflows
### Creating a New Model
1. **Validate the source first:**
```bash
curl -X POST "/v1/dsl/validate/model" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"source": "{ \"name\": \"product\", \"table\": { \"name\": \"products\" }, \"columns\": [{ \"name\": \"id\", \"type\": \"ID\" }] }"
}'
```
2. **Create the model:**
```bash
curl -X POST "/v1/dsl/create/model" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"id": "product",
"source": "{ \"name\": \"product\", \"table\": { \"name\": \"products\" }, \"columns\": [{ \"name\": \"id\", \"type\": \"ID\" }] }",
"store": "file"
}'
```
3. **Verify it was created:**
```bash
curl -X GET "/v1/dsl/inspect/model/product" \
-H "Authorization: Bearer {token}"
```
### Updating and Reloading a DSL
1. **Update the DSL:**
```bash
curl -X PUT "/v1/dsl/update/model" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"id": "product",
"source": "{ \"name\": \"product\", \"label\": \"Product Model\", \"table\": { \"name\": \"products\" }, \"columns\": [{ \"name\": \"id\", \"type\": \"ID\" }, { \"name\": \"name\", \"type\": \"string\" }] }"
}'
```
2. **Reload to apply changes:**
```bash
curl -X POST "/v1/dsl/reload/model" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"id": "product",
"store": "file"
}'
```
This API provides comprehensive DSL management capabilities that align with the test cases and interface definitions in the codebase.

442
openapi/dsl/dsl.go Normal file
View file

@ -0,0 +1,442 @@
package dsl
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/yaoapp/yao/dsl"
"github.com/yaoapp/yao/dsl/types"
oauthTypes "github.com/yaoapp/yao/openapi/oauth/types"
)
// Yao DSL Manager API
// Attach attaches the DSL management handlers to the router
func Attach(group *gin.RouterGroup, oauth oauthTypes.OAuth) {
// Protect all endpoints with OAuth
group.Handlers = append(group.Handlers, oauth.Guard)
// DSL Information endpoints
group.GET("/inspect/:type/:id", inspect)
group.GET("/source/:type/:id", source)
group.GET("/path/:type/:id", path)
group.GET("/list/:type", list)
group.GET("/exists/:type/:id", exists)
// DSL CRUD operations
group.POST("/create/:type", create)
group.PUT("/update/:type", update)
group.DELETE("/delete/:type/:id", delete)
// DSL Load management
group.POST("/load/:type", load)
group.POST("/unload/:type", unload)
group.POST("/reload/:type", reload)
// DSL Execute and Validate
group.POST("/execute/:type/:id/:method", execute)
group.POST("/validate/:type", validate)
}
// Inspect DSL information
func inspect(c *gin.Context) {
dslType := types.Type(c.Param("type"))
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "DSL ID is required"})
return
}
dslManager, err := dsl.New(dslType)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)})
return
}
info, err := dslManager.Inspect(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, info)
}
// Get DSL source code
func source(c *gin.Context) {
dslType := types.Type(c.Param("type"))
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "DSL ID is required"})
return
}
dslManager, err := dsl.New(dslType)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)})
return
}
sourceCode, err := dslManager.Source(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"source": sourceCode})
}
// Get DSL file path
func path(c *gin.Context) {
dslType := types.Type(c.Param("type"))
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "DSL ID is required"})
return
}
dslManager, err := dsl.New(dslType)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)})
return
}
filePath, err := dslManager.Path(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"path": filePath})
}
// List DSLs with optional filters
func list(c *gin.Context) {
dslType := types.Type(c.Param("type"))
dslManager, err := dsl.New(dslType)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)})
return
}
// Parse query parameters
opts := &types.ListOptions{
Sort: c.Query("sort"),
Order: c.Query("order"),
Store: types.StoreType(c.Query("store")),
Pattern: c.Query("pattern"),
}
// Parse source flag
if sourceStr := c.Query("source"); sourceStr != "" {
if sourceBool, err := strconv.ParseBool(sourceStr); err == nil {
opts.Source = sourceBool
}
}
// Parse tags from query parameter (comma-separated)
if tagsStr := c.Query("tags"); tagsStr != "" {
c.ShouldBindQuery(&struct {
Tags []string `form:"tags"`
}{Tags: opts.Tags})
}
infos, err := dslManager.List(c.Request.Context(), opts)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, infos)
}
// Check if DSL exists
func exists(c *gin.Context) {
dslType := types.Type(c.Param("type"))
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "DSL ID is required"})
return
}
dslManager, err := dsl.New(dslType)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)})
return
}
exist, err := dslManager.Exists(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"exists": exist})
}
// Create a new DSL
func create(c *gin.Context) {
dslType := types.Type(c.Param("type"))
dslManager, err := dsl.New(dslType)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)})
return
}
var options types.CreateOptions
if err := c.ShouldBindJSON(&options); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body: " + err.Error()})
return
}
if options.ID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "DSL ID is required"})
return
}
if options.Source == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "DSL source is required"})
return
}
err = dslManager.Create(c.Request.Context(), &options)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"message": "DSL created successfully"})
}
// Update an existing DSL
func update(c *gin.Context) {
dslType := types.Type(c.Param("type"))
dslManager, err := dsl.New(dslType)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)})
return
}
var options types.UpdateOptions
if err := c.ShouldBindJSON(&options); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body: " + err.Error()})
return
}
if options.ID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "DSL ID is required"})
return
}
if options.Source == "" && options.Info == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "DSL source or info is required"})
return
}
err = dslManager.Update(c.Request.Context(), &options)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "DSL updated successfully"})
}
// Delete a DSL
func delete(c *gin.Context) {
dslType := types.Type(c.Param("type"))
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "DSL ID is required"})
return
}
dslManager, err := dsl.New(dslType)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)})
return
}
// Parse optional request body for delete options
var options types.DeleteOptions
options.ID = id
// Try to bind JSON body if provided
c.ShouldBindJSON(&options)
err = dslManager.Delete(c.Request.Context(), &options)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "DSL deleted successfully"})
}
// Load a DSL
func load(c *gin.Context) {
dslType := types.Type(c.Param("type"))
dslManager, err := dsl.New(dslType)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)})
return
}
var options types.LoadOptions
if err := c.ShouldBindJSON(&options); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body: " + err.Error()})
return
}
if options.ID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "DSL ID is required"})
return
}
err = dslManager.Load(c.Request.Context(), &options)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "DSL loaded successfully"})
}
// Unload a DSL
func unload(c *gin.Context) {
dslType := types.Type(c.Param("type"))
dslManager, err := dsl.New(dslType)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)})
return
}
var options types.UnloadOptions
if err := c.ShouldBindJSON(&options); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body: " + err.Error()})
return
}
if options.ID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "DSL ID is required"})
return
}
err = dslManager.Unload(c.Request.Context(), &options)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "DSL unloaded successfully"})
}
// Reload a DSL
func reload(c *gin.Context) {
dslType := types.Type(c.Param("type"))
dslManager, err := dsl.New(dslType)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)})
return
}
var options types.ReloadOptions
if err := c.ShouldBindJSON(&options); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body: " + err.Error()})
return
}
if options.ID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "DSL ID is required"})
return
}
err = dslManager.Reload(c.Request.Context(), &options)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "DSL reloaded successfully"})
}
// Execute a DSL method
func execute(c *gin.Context) {
dslType := types.Type(c.Param("type"))
id := c.Param("id")
method := c.Param("method")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "DSL ID is required"})
return
}
if method == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Method name is required"})
return
}
dslManager, err := dsl.New(dslType)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)})
return
}
// Parse arguments from request body
var requestBody struct {
Args []interface{} `json:"args"`
}
if err := c.ShouldBindJSON(&requestBody); err != nil {
// If no body provided, execute without arguments
requestBody.Args = []interface{}{}
}
result, err := dslManager.Execute(c.Request.Context(), id, method, requestBody.Args...)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"result": result})
}
// Validate DSL source code
func validate(c *gin.Context) {
dslType := types.Type(c.Param("type"))
dslManager, err := dsl.New(dslType)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)})
return
}
var requestBody struct {
Source string `json:"source" binding:"required"`
}
if err := c.ShouldBindJSON(&requestBody); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "DSL source is required"})
return
}
valid, messages := dslManager.Validate(c.Request.Context(), requestBody.Source)
c.JSON(http.StatusOK, gin.H{
"valid": valid,
"messages": messages,
})
}

712
openapi/dsl_test.go Normal file
View file

@ -0,0 +1,712 @@
package openapi
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/yao/dsl/types"
)
// TestDSLCreate tests the DSL creation endpoint
func TestDSLCreate(t *testing.T) {
serverURL := Prepare(t)
defer Clean()
// Get base URL from server config
baseURL := ""
if Server != nil && Server.Config != nil {
baseURL = Server.Config.BaseURL
}
// Register test client and get token
client := RegisterTestClient(t, "DSL Create Test Client", []string{"https://localhost/callback"})
defer CleanupTestClient(t, client.ClientID)
tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
// Generate unique test ID
testID := fmt.Sprintf("test_model_%d", time.Now().UnixNano())
// Model test data
modelSource := fmt.Sprintf(`{
"name": "%s",
"table": { "name": "%s", "comment": "Test User Model" },
"columns": [
{ "name": "id", "type": "ID" },
{ "name": "name", "type": "string", "length": 80, "comment": "User Name", "index": true },
{ "name": "status", "type": "enum", "option": ["active", "disabled"], "default": "active", "comment": "Status", "index": true }
],
"tags": ["test_%s"],
"label": "Test Model",
"description": "Test Model Description",
"option": { "timestamps": true, "soft_deletes": true }
}`, testID, testID, testID)
// Test creation with different stores
stores := []string{"db", "file"}
for _, store := range stores {
t.Run(fmt.Sprintf("CreateModel_%s", store), func(t *testing.T) {
// Prepare request body
createData := map[string]interface{}{
"id": testID + "_" + store,
"source": modelSource,
"store": store,
}
body, err := json.Marshal(createData)
assert.NoError(t, err)
// Create HTTP request
req, err := http.NewRequest("POST", serverURL+baseURL+"/dsl/create/model", bytes.NewBuffer(body))
assert.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
// Make request
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
assert.NotNil(t, resp)
defer resp.Body.Close()
// Check response
assert.Equal(t, http.StatusCreated, resp.StatusCode)
var response map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&response)
assert.NoError(t, err)
assert.Equal(t, "DSL created successfully", response["message"])
t.Logf("Successfully created model DSL with store: %s", store)
})
}
}
// TestDSLInspect tests the DSL inspection endpoint
func TestDSLInspect(t *testing.T) {
serverURL := Prepare(t)
defer Clean()
baseURL := ""
if Server != nil && Server.Config != nil {
baseURL = Server.Config.BaseURL
}
client := RegisterTestClient(t, "DSL Inspect Test Client", []string{"https://localhost/callback"})
defer CleanupTestClient(t, client.ClientID)
tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
testID := fmt.Sprintf("test_inspect_%d", time.Now().UnixNano())
modelSource := fmt.Sprintf(`{
"name": "%s",
"table": { "name": "%s", "comment": "Test Inspect Model" },
"columns": [
{ "name": "id", "type": "ID" },
{ "name": "name", "type": "string", "length": 80, "comment": "User Name", "index": true }
],
"tags": ["test_inspect"],
"label": "Test Inspect Model",
"description": "Test Model for Inspection",
"option": { "timestamps": true }
}`, testID, testID)
// First create a model
createData := map[string]interface{}{
"id": testID,
"source": modelSource,
"store": "db",
}
body, _ := json.Marshal(createData)
req, _ := http.NewRequest("POST", serverURL+baseURL+"/dsl/create/model", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
createResp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
defer createResp.Body.Close()
assert.Equal(t, http.StatusCreated, createResp.StatusCode)
// Now test inspection
req, err = http.NewRequest("GET", serverURL+baseURL+"/dsl/inspect/model/"+testID, nil)
assert.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
assert.NotNil(t, resp)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var info types.Info
err = json.NewDecoder(resp.Body).Decode(&info)
assert.NoError(t, err)
// Verify the inspection results
assert.Equal(t, testID, info.ID)
assert.Equal(t, types.TypeModel, info.Type)
assert.Equal(t, "Test Inspect Model", info.Label)
assert.Equal(t, "Test Model for Inspection", info.Description)
assert.Contains(t, info.Tags, "test_inspect")
assert.False(t, info.Readonly)
assert.False(t, info.Builtin)
t.Logf("Successfully inspected model DSL: %+v", info)
}
// TestDSLSource tests the DSL source retrieval endpoint
func TestDSLSource(t *testing.T) {
serverURL := Prepare(t)
defer Clean()
baseURL := ""
if Server != nil && Server.Config != nil {
baseURL = Server.Config.BaseURL
}
client := RegisterTestClient(t, "DSL Source Test Client", []string{"https://localhost/callback"})
defer CleanupTestClient(t, client.ClientID)
tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
testID := fmt.Sprintf("test_source_%d", time.Now().UnixNano())
modelSource := fmt.Sprintf(`{
"name": "%s",
"table": { "name": "%s", "comment": "Test Source Model" },
"columns": [
{ "name": "id", "type": "ID" },
{ "name": "email", "type": "string", "length": 100, "comment": "Email", "index": true }
],
"tags": ["test_source"],
"label": "Test Source Model",
"description": "Test Model for Source Retrieval"
}`, testID, testID)
// Create model first
createData := map[string]interface{}{
"id": testID,
"source": modelSource,
"store": "db",
}
body, err := json.Marshal(createData)
assert.NoError(t, err)
req, err := http.NewRequest("POST", serverURL+baseURL+"/dsl/create/model", bytes.NewBuffer(body))
assert.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
createResp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
assert.NotNil(t, createResp)
defer createResp.Body.Close()
// Test source retrieval
req, err = http.NewRequest("GET", serverURL+baseURL+"/dsl/source/model/"+testID, nil)
assert.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
assert.NotNil(t, resp)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var response map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&response)
assert.NoError(t, err)
sourceReturned := response["source"].(string)
assert.Equal(t, modelSource, sourceReturned)
t.Logf("Successfully retrieved model DSL source")
}
// TestDSLList tests the DSL listing endpoint
func TestDSLList(t *testing.T) {
serverURL := Prepare(t)
defer Clean()
baseURL := ""
if Server != nil && Server.Config != nil {
baseURL = Server.Config.BaseURL
}
client := RegisterTestClient(t, "DSL List Test Client", []string{"https://localhost/callback"})
defer CleanupTestClient(t, client.ClientID)
tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
// Create multiple test models
testTag := fmt.Sprintf("test_list_%d", time.Now().UnixNano())
for i := 0; i < 3; i++ {
testID := fmt.Sprintf("test_list_model_%d_%d", time.Now().UnixNano(), i)
modelSource := fmt.Sprintf(`{
"name": "%s",
"table": { "name": "%s", "comment": "Test List Model %d" },
"columns": [
{ "name": "id", "type": "ID" },
{ "name": "title", "type": "string", "length": 100 }
],
"tags": ["%s"],
"label": "Test List Model %d"
}`, testID, testID, i, testTag, i)
createData := map[string]interface{}{
"id": testID,
"source": modelSource,
"store": "db",
}
body, err := json.Marshal(createData)
assert.NoError(t, err)
req, err := http.NewRequest("POST", serverURL+baseURL+"/dsl/create/model", bytes.NewBuffer(body))
assert.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
createResp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
assert.NotNil(t, createResp)
createResp.Body.Close()
}
// Test listing all models
req, err := http.NewRequest("GET", serverURL+baseURL+"/dsl/list/model", nil)
assert.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
assert.NotNil(t, resp)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var data []interface{}
err = json.NewDecoder(resp.Body).Decode(&data)
assert.NoError(t, err)
assert.GreaterOrEqual(t, len(data), 3, "Should have at least 3 models")
// Test listing with tags filter
req, err = http.NewRequest("GET", serverURL+baseURL+"/dsl/list/model?tags="+testTag, nil)
assert.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
resp, err = http.DefaultClient.Do(req)
assert.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var taggedData []interface{}
err = json.NewDecoder(resp.Body).Decode(&taggedData)
assert.NoError(t, err)
assert.GreaterOrEqual(t, len(taggedData), 3, "Should find the tagged models")
t.Logf("Successfully listed %d model DSLs", len(data))
}
// TestDSLUpdate tests the DSL update endpoint
func TestDSLUpdate(t *testing.T) {
serverURL := Prepare(t)
defer Clean()
baseURL := ""
if Server != nil && Server.Config != nil {
baseURL = Server.Config.BaseURL
}
client := RegisterTestClient(t, "DSL Update Test Client", []string{"https://localhost/callback"})
defer CleanupTestClient(t, client.ClientID)
tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
testID := fmt.Sprintf("test_update_%d", time.Now().UnixNano())
// Original model
originalSource := fmt.Sprintf(`{
"name": "%s",
"table": { "name": "%s", "comment": "Original Model" },
"columns": [
{ "name": "id", "type": "ID" },
{ "name": "name", "type": "string", "length": 80 }
],
"tags": ["test_update"],
"label": "Original Model"
}`, testID, testID)
// Updated model
updatedSource := fmt.Sprintf(`{
"name": "%s",
"table": { "name": "%s", "comment": "Updated Model" },
"columns": [
{ "name": "id", "type": "ID" },
{ "name": "name", "type": "string", "length": 80 },
{ "name": "email", "type": "string", "length": 100 }
],
"tags": ["test_update", "updated"],
"label": "Updated Model",
"description": "Updated model description"
}`, testID, testID)
// Create original model
createData := map[string]interface{}{
"id": testID,
"source": originalSource,
"store": "db",
}
body, err := json.Marshal(createData)
assert.NoError(t, err)
req, err := http.NewRequest("POST", serverURL+baseURL+"/dsl/create/model", bytes.NewBuffer(body))
assert.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
createResp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
assert.NotNil(t, createResp)
createResp.Body.Close()
// Update model
updateData := map[string]interface{}{
"id": testID,
"source": updatedSource,
}
body, err = json.Marshal(updateData)
assert.NoError(t, err)
req, err = http.NewRequest("PUT", serverURL+baseURL+"/dsl/update/model", bytes.NewBuffer(body))
assert.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
assert.NotNil(t, resp)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var response map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&response)
assert.NoError(t, err)
assert.Equal(t, "DSL updated successfully", response["message"])
// Verify the update by inspecting the model
req, err = http.NewRequest("GET", serverURL+baseURL+"/dsl/inspect/model/"+testID, nil)
assert.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
inspectResp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
assert.NotNil(t, inspectResp)
defer inspectResp.Body.Close()
var info types.Info
err = json.NewDecoder(inspectResp.Body).Decode(&info)
assert.NoError(t, err)
assert.Equal(t, "Updated Model", info.Label)
assert.Equal(t, "Updated model description", info.Description)
assert.Contains(t, info.Tags, "updated")
t.Logf("Successfully updated model DSL")
}
// TestDSLExists tests the DSL existence check endpoint
func TestDSLExists(t *testing.T) {
serverURL := Prepare(t)
defer Clean()
baseURL := ""
if Server != nil && Server.Config != nil {
baseURL = Server.Config.BaseURL
}
client := RegisterTestClient(t, "DSL Exists Test Client", []string{"https://localhost/callback"})
defer CleanupTestClient(t, client.ClientID)
tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
testID := fmt.Sprintf("test_exists_%d", time.Now().UnixNano())
nonExistentID := fmt.Sprintf("non_existent_%d", time.Now().UnixNano())
// Test non-existent model first
req, err := http.NewRequest("GET", serverURL+baseURL+"/dsl/exists/model/"+nonExistentID, nil)
assert.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var response map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&response)
assert.NoError(t, err)
assert.False(t, response["exists"].(bool))
// Create a model
modelSource := fmt.Sprintf(`{
"name": "%s",
"table": { "name": "%s" },
"columns": [{"name": "id", "type": "ID"}]
}`, testID, testID)
createData := map[string]interface{}{
"id": testID,
"source": modelSource,
"store": "db",
}
body, err := json.Marshal(createData)
assert.NoError(t, err)
createReq, err := http.NewRequest("POST", serverURL+baseURL+"/dsl/create/model", bytes.NewBuffer(body))
assert.NoError(t, err)
createReq.Header.Set("Content-Type", "application/json")
createReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
createResp, err := http.DefaultClient.Do(createReq)
assert.NoError(t, err)
assert.NotNil(t, createResp)
createResp.Body.Close()
// Test existing model
req, err = http.NewRequest("GET", serverURL+baseURL+"/dsl/exists/model/"+testID, nil)
assert.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
resp, err = http.DefaultClient.Do(req)
assert.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
err = json.NewDecoder(resp.Body).Decode(&response)
assert.NoError(t, err)
assert.True(t, response["exists"].(bool))
t.Logf("Successfully tested model DSL existence")
}
// TestDSLDelete tests the DSL deletion endpoint
func TestDSLDelete(t *testing.T) {
serverURL := Prepare(t)
defer Clean()
baseURL := ""
if Server != nil && Server.Config != nil {
baseURL = Server.Config.BaseURL
}
client := RegisterTestClient(t, "DSL Delete Test Client", []string{"https://localhost/callback"})
defer CleanupTestClient(t, client.ClientID)
tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
testID := fmt.Sprintf("test_delete_%d", time.Now().UnixNano())
// Create a model first
modelSource := fmt.Sprintf(`{
"name": "%s",
"table": { "name": "%s" },
"columns": [{"name": "id", "type": "ID"}]
}`, testID, testID)
createData := map[string]interface{}{
"id": testID,
"source": modelSource,
"store": "db",
}
body, err := json.Marshal(createData)
assert.NoError(t, err)
createReq, err := http.NewRequest("POST", serverURL+baseURL+"/dsl/create/model", bytes.NewBuffer(body))
assert.NoError(t, err)
createReq.Header.Set("Content-Type", "application/json")
createReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
createResp, err := http.DefaultClient.Do(createReq)
assert.NoError(t, err)
assert.NotNil(t, createResp)
createResp.Body.Close()
// Delete the model
req, err := http.NewRequest("DELETE", serverURL+baseURL+"/dsl/delete/model/"+testID, nil)
assert.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
assert.NotNil(t, resp)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var response map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&response)
assert.NoError(t, err)
assert.Equal(t, "DSL deleted successfully", response["message"])
// Verify deletion by checking existence
existsReq, err := http.NewRequest("GET", serverURL+baseURL+"/dsl/exists/model/"+testID, nil)
assert.NoError(t, err)
existsReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
existsResp, err := http.DefaultClient.Do(existsReq)
assert.NoError(t, err)
assert.NotNil(t, existsResp)
defer existsResp.Body.Close()
var existsResponse map[string]interface{}
err = json.NewDecoder(existsResp.Body).Decode(&existsResponse)
assert.NoError(t, err)
assert.False(t, existsResponse["exists"].(bool))
t.Logf("Successfully deleted model DSL")
}
// TestDSLValidate tests the DSL validation endpoint
func TestDSLValidate(t *testing.T) {
serverURL := Prepare(t)
defer Clean()
baseURL := ""
if Server != nil && Server.Config != nil {
baseURL = Server.Config.BaseURL
}
client := RegisterTestClient(t, "DSL Validate Test Client", []string{"https://localhost/callback"})
defer CleanupTestClient(t, client.ClientID)
tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
tests := []struct {
name string
source string
description string
}{
{
name: "ValidModel",
source: `{
"name": "valid_model",
"table": { "name": "valid_model" },
"columns": [
{"name": "id", "type": "ID"},
{"name": "name", "type": "string", "length": 80}
]
}`,
description: "Valid model definition",
},
{
name: "AnotherModel",
source: `{
"name": "another_model",
"table": { "name": "another_model" },
"columns": [
{"name": "id", "type": "ID"},
{"name": "title", "type": "string", "length": 100}
]
}`,
description: "Another model definition",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
requestBody := map[string]string{
"source": tt.source,
}
body, err := json.Marshal(requestBody)
assert.NoError(t, err)
req, err := http.NewRequest("POST", serverURL+baseURL+"/dsl/validate/model", bytes.NewBuffer(body))
assert.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
assert.NotNil(t, resp)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var response map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&response)
assert.NoError(t, err)
// Just verify the response has the expected structure
assert.Contains(t, response, "valid")
assert.Contains(t, response, "messages")
valid, ok := response["valid"].(bool)
assert.True(t, ok, "valid should be a boolean")
if messages, ok := response["messages"]; ok {
t.Logf("Validation messages for %s: %v", tt.description, messages)
}
t.Logf("Successfully validated %s: valid=%v", tt.description, valid)
})
}
}
// TestDSLUnauthorized tests that endpoints return 401 when not authenticated
func TestDSLUnauthorized(t *testing.T) {
serverURL := Prepare(t)
defer Clean()
baseURL := ""
if Server != nil && Server.Config != nil {
baseURL = Server.Config.BaseURL
}
endpoints := []struct {
method string
path string
body string
}{
{"GET", "/dsl/inspect/model/test", ""},
{"GET", "/dsl/source/model/test", ""},
{"GET", "/dsl/list/model", ""},
{"GET", "/dsl/exists/model/test", ""},
{"POST", "/dsl/create/model", `{"id":"test","source":"{}"}`},
{"PUT", "/dsl/update/model", `{"id":"test","source":"{}"}`},
{"DELETE", "/dsl/delete/model/test", ""},
{"POST", "/dsl/validate/model", `{"source":"{}"}`},
}
for _, endpoint := range endpoints {
t.Run(fmt.Sprintf("Unauthorized_%s_%s", endpoint.method, endpoint.path), func(t *testing.T) {
var req *http.Request
var err error
if endpoint.body != "" {
req, err = http.NewRequest(endpoint.method, serverURL+baseURL+endpoint.path, bytes.NewBufferString(endpoint.body))
req.Header.Set("Content-Type", "application/json")
} else {
req, err = http.NewRequest(endpoint.method, serverURL+baseURL+endpoint.path, nil)
}
assert.NoError(t, err)
// No Authorization header
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
assert.NotNil(t, resp)
defer resp.Body.Close()
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
t.Logf("Correctly rejected unauthorized request to %s %s", endpoint.method, endpoint.path)
})
}
}

270
openapi/hello/README.md Normal file
View file

@ -0,0 +1,270 @@
# Hello World API
Simple endpoints for testing connectivity, server status, and OAuth authentication functionality.
## Base URL
All endpoints are prefixed with the configured base URL followed by `/helloworld` (e.g., `/v1/helloworld`).
## Endpoints
### Public Endpoint
Test basic server connectivity without authentication.
```
GET /helloworld/public
POST /helloworld/public
```
**No authentication required.**
**Example:**
```bash
curl -X GET "/v1/helloworld/public"
```
**Response:**
```json
{
"MESSAGE": "HELLO, WORLD",
"SERVER_TIME": "2024-01-15T10:30:00Z",
"VERSION": "1.0.0",
"PRVERSION": "1.0.0-preview",
"CUI": "1.0.0",
"PRCUI": "1.0.0-preview",
"APP": "YaoApp",
"APP_VERSION": "1.0.0"
}
```
**Response Fields:**
- `MESSAGE` - Static "HELLO, WORLD" message
- `SERVER_TIME` - Current server time in RFC3339 format
- `VERSION` - Yao framework version
- `PRVERSION` - Yao framework preview version
- `CUI` - Yao CUI version
- `PRCUI` - Yao CUI preview version
- `APP` - Application name
- `APP_VERSION` - Application version
### Protected Endpoint
Test OAuth authentication and authorization.
```
GET /helloworld/protected
POST /helloworld/protected
```
**Authentication:** Required (OAuth Bearer token)
**Headers:**
```
Authorization: Bearer {access_token}
```
**Example:**
```bash
# First, obtain an access token
curl -X POST "/v1/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=your_client_id&client_secret=your_client_secret"
# Then use the token to access the protected endpoint
curl -X GET "/v1/helloworld/protected" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
```
**Response:**
Same response format as the public endpoint, confirming that authentication is working correctly.
```json
{
"MESSAGE": "HELLO, WORLD",
"SERVER_TIME": "2024-01-15T10:30:00Z",
"VERSION": "1.0.0",
"PRVERSION": "1.0.0-preview",
"CUI": "1.0.0",
"PRCUI": "1.0.0-preview",
"APP": "YaoApp",
"APP_VERSION": "1.0.0"
}
```
## HTTP Methods
Both endpoints support both GET and POST methods, allowing flexibility for different client requirements and testing scenarios.
## Error Responses
### Unauthorized Access
When accessing the protected endpoint without proper authentication:
**HTTP Status:** `401 Unauthorized`
**Response:**
```json
{
"error": "invalid_token",
"error_description": "The access token provided is expired, revoked, malformed, or invalid"
}
```
### Invalid Token
When using an invalid or expired token:
**HTTP Status:** `401 Unauthorized`
**Response:**
```json
{
"error": "invalid_token",
"error_description": "The access token provided is invalid"
}
```
## Use Cases
### Health Check
Use the public endpoint as a health check for monitoring systems:
```bash
# Simple health check
curl -f "/v1/helloworld/public" > /dev/null 2>&1 && echo "Service is healthy" || echo "Service is down"
```
### Authentication Testing
Use the protected endpoint to verify OAuth authentication setup:
```bash
# Test authentication flow
TOKEN=$(curl -s -X POST "/v1/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET" \
| jq -r '.access_token')
curl -X GET "/v1/helloworld/protected" \
-H "Authorization: Bearer $TOKEN"
```
### Development and Debugging
These endpoints are useful for:
- **API Gateway Testing** - Verify routing and load balancer configuration
- **Authentication Debugging** - Test OAuth token validation
- **Environment Verification** - Check server version and configuration
- **Network Connectivity** - Basic reachability testing
- **Performance Baseline** - Minimal response time measurement
## Integration Examples
### JavaScript/Browser
```javascript
// Public endpoint
fetch("/v1/helloworld/public")
.then((response) => response.json())
.then((data) => console.log(data));
// Protected endpoint
const token = localStorage.getItem("access_token");
fetch("/v1/helloworld/protected", {
headers: {
Authorization: `Bearer ${token}`,
},
})
.then((response) => response.json())
.then((data) => console.log(data));
```
### Python
```python
import requests
# Public endpoint
response = requests.get('/v1/helloworld/public')
print(response.json())
# Protected endpoint
headers = {'Authorization': f'Bearer {access_token}'}
response = requests.get('/v1/helloworld/protected', headers=headers)
print(response.json())
```
### Go
```go
package main
import (
"fmt"
"io"
"net/http"
)
func testPublicEndpoint() {
resp, err := http.Get("/v1/helloworld/public")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
func testProtectedEndpoint(token string) {
req, _ := http.NewRequest("GET", "/v1/helloworld/protected", nil)
req.Header.Set("Authorization", "Bearer "+token)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
```
## Security Considerations
### Public Endpoint
- No sensitive information is exposed
- Safe for use in monitoring and health checks
- Should be rate-limited to prevent abuse
### Protected Endpoint
- Requires valid OAuth access token
- Validates token signature and expiration
- Can be used to test authorization scopes (if implemented)
## Server Information
The response includes various version and application information useful for:
- **Version Compatibility** - Ensure client compatibility with server version
- **Environment Identification** - Distinguish between development, staging, and production
- **Debugging** - Identify exact server version when reporting issues
- **Monitoring** - Track server deployments and version rollouts
This API serves as a foundation for testing and validating the Yao OpenAPI infrastructure.

View file

@ -1,30 +1,27 @@
package openapi
package hello
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/yaoapp/yao/openapi/oauth/types"
"github.com/yaoapp/yao/share"
)
// attachHelloWorld attaches the hello world handlers to the router
func (openapi *OpenAPI) attachHelloWorld(base *gin.RouterGroup) {
// hello handlers
hello := base.Group("/helloworld")
// Attach attaches the hello world handlers to the router
func Attach(group *gin.RouterGroup, oauth types.OAuth) {
// Health check
hello.GET("/public", openapi.helloWorldPublic)
hello.POST("/public", openapi.helloWorldPublic)
group.GET("/public", helloWorldPublic)
group.POST("/public", helloWorldPublic)
// OAuth Protected Resource
hello.GET("/protected", openapi.OAuth.Guard, openapi.helloWorldProtected)
hello.POST("/protected", openapi.OAuth.Guard, openapi.helloWorldProtected)
group.GET("/protected", oauth.Guard, helloWorldProtected)
group.POST("/protected", oauth.Guard, helloWorldProtected)
}
// helloWorldPublic is the handler for the hello world endpoint
func (openapi *OpenAPI) helloWorldPublic(c *gin.Context) {
func helloWorldPublic(c *gin.Context) {
serverTime := time.Now().Format(time.RFC3339)
c.JSON(http.StatusOK, gin.H{
"MESSAGE": "HELLO, WORLD",
@ -39,7 +36,7 @@ func (openapi *OpenAPI) helloWorldPublic(c *gin.Context) {
}
// helloWorldHello is the handler for the hello world endpoint
func (openapi *OpenAPI) helloWorldProtected(c *gin.Context) {
func helloWorldProtected(c *gin.Context) {
serverTime := time.Now().Format(time.RFC3339)
c.JSON(http.StatusOK, gin.H{
"MESSAGE": "HELLO, WORLD",

View file

@ -6,6 +6,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/yaoapp/gou/application"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/openapi/dsl"
"github.com/yaoapp/yao/openapi/hello"
"github.com/yaoapp/yao/openapi/oauth"
"github.com/yaoapp/yao/openapi/oauth/types"
)
@ -71,7 +73,10 @@ func (openapi *OpenAPI) Attach(router *gin.Engine) {
openapi.attachOAuth(group)
// Hello World handlers
openapi.attachHelloWorld(group)
hello.Attach(group.Group("/helloworld"), openapi.OAuth)
// DSL handlers
dsl.Attach(group.Group("/dsl"), openapi.OAuth)
// Custom handlers (Defined by developer)
}