Merge pull request #1030 from trheyi/main
Remove hello world endpoints and refactor routing to use new hello pa…
This commit is contained in:
commit
d3077c217c
9 changed files with 2539 additions and 18 deletions
107
openapi/COMMERCIAL.md
Normal file
107
openapi/COMMERCIAL.md
Normal 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
107
openapi/COMMERCIAL.zh-CN.md
Normal 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_
|
||||||
|
|
@ -1,9 +1,400 @@
|
||||||
# Yao OpenAPI
|
# 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
490
openapi/dsl/README.md
Normal 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
442
openapi/dsl/dsl.go
Normal 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
712
openapi/dsl_test.go
Normal 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
270
openapi/hello/README.md
Normal 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.
|
||||||
|
|
@ -1,30 +1,27 @@
|
||||||
package openapi
|
package hello
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
"github.com/yaoapp/yao/share"
|
"github.com/yaoapp/yao/share"
|
||||||
)
|
)
|
||||||
|
|
||||||
// attachHelloWorld attaches the hello world handlers to the router
|
// Attach attaches the hello world handlers to the router
|
||||||
func (openapi *OpenAPI) attachHelloWorld(base *gin.RouterGroup) {
|
func Attach(group *gin.RouterGroup, oauth types.OAuth) {
|
||||||
|
|
||||||
// hello handlers
|
|
||||||
hello := base.Group("/helloworld")
|
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
hello.GET("/public", openapi.helloWorldPublic)
|
group.GET("/public", helloWorldPublic)
|
||||||
hello.POST("/public", openapi.helloWorldPublic)
|
group.POST("/public", helloWorldPublic)
|
||||||
|
|
||||||
// OAuth Protected Resource
|
// OAuth Protected Resource
|
||||||
hello.GET("/protected", openapi.OAuth.Guard, openapi.helloWorldProtected)
|
group.GET("/protected", oauth.Guard, helloWorldProtected)
|
||||||
hello.POST("/protected", openapi.OAuth.Guard, openapi.helloWorldProtected)
|
group.POST("/protected", oauth.Guard, helloWorldProtected)
|
||||||
}
|
}
|
||||||
|
|
||||||
// helloWorldPublic is the handler for the hello world endpoint
|
func helloWorldPublic(c *gin.Context) {
|
||||||
func (openapi *OpenAPI) helloWorldPublic(c *gin.Context) {
|
|
||||||
serverTime := time.Now().Format(time.RFC3339)
|
serverTime := time.Now().Format(time.RFC3339)
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"MESSAGE": "HELLO, WORLD",
|
"MESSAGE": "HELLO, WORLD",
|
||||||
|
|
@ -39,7 +36,7 @@ func (openapi *OpenAPI) helloWorldPublic(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// helloWorldHello is the handler for the hello world endpoint
|
// 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)
|
serverTime := time.Now().Format(time.RFC3339)
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"MESSAGE": "HELLO, WORLD",
|
"MESSAGE": "HELLO, WORLD",
|
||||||
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/yaoapp/gou/application"
|
"github.com/yaoapp/gou/application"
|
||||||
"github.com/yaoapp/yao/config"
|
"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"
|
||||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
)
|
)
|
||||||
|
|
@ -71,7 +73,10 @@ func (openapi *OpenAPI) Attach(router *gin.Engine) {
|
||||||
openapi.attachOAuth(group)
|
openapi.attachOAuth(group)
|
||||||
|
|
||||||
// Hello World handlers
|
// 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)
|
// Custom handlers (Defined by developer)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue