Initial commit
This commit is contained in:
231
skills/api.define/SKILL.md
Normal file
231
skills/api.define/SKILL.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# api.define
|
||||
|
||||
## Overview
|
||||
|
||||
**api.define** scaffolds OpenAPI and AsyncAPI specifications from enterprise-compliant templates, generating production-ready API contracts with best practices built-in.
|
||||
|
||||
## Purpose
|
||||
|
||||
Quickly create API specifications that follow enterprise guidelines:
|
||||
- Generate Zalando-compliant OpenAPI 3.1 specs
|
||||
- Generate AsyncAPI 3.0 specs for event-driven APIs
|
||||
- Include proper error handling (RFC 7807 Problem JSON)
|
||||
- Use correct naming conventions (snake_case)
|
||||
- Include required metadata and security schemes
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```bash
|
||||
python skills/api.define/api_define.py <service_name> [spec_type] [options]
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Required | Description | Default |
|
||||
|-----------|----------|-------------|---------|
|
||||
| `service_name` | Yes | Service/API name | - |
|
||||
| `spec_type` | No | openapi or asyncapi | `openapi` |
|
||||
| `--template` | No | Template name | `zalando` |
|
||||
| `--output-dir` | No | Output directory | `specs` |
|
||||
| `--version` | No | API version | `1.0.0` |
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Create Zalando-Compliant OpenAPI Spec
|
||||
|
||||
```bash
|
||||
python skills/api.define/api_define.py user-service openapi --template=zalando
|
||||
```
|
||||
|
||||
**Output**: `specs/user-service.openapi.yaml`
|
||||
|
||||
Generated spec includes:
|
||||
- ✅ Required Zalando metadata (`x-api-id`, `x-audience`)
|
||||
- ✅ CRUD operations for users resource
|
||||
- ✅ RFC 7807 Problem JSON for errors
|
||||
- ✅ snake_case property names
|
||||
- ✅ X-Flow-ID headers for tracing
|
||||
- ✅ Proper HTTP status codes
|
||||
- ✅ JWT authentication scheme
|
||||
|
||||
### Example 2: Create AsyncAPI Spec
|
||||
|
||||
```bash
|
||||
python skills/api.define/api_define.py user-service asyncapi
|
||||
```
|
||||
|
||||
**Output**: `specs/user-service.asyncapi.yaml`
|
||||
|
||||
Generated spec includes:
|
||||
- ✅ Lifecycle events (created, updated, deleted)
|
||||
- ✅ Kafka channel definitions
|
||||
- ✅ Event payload schemas
|
||||
- ✅ Publish/subscribe operations
|
||||
|
||||
### Example 3: Custom Output Directory
|
||||
|
||||
```bash
|
||||
python skills/api.define/api_define.py order-api openapi \
|
||||
--output-dir=api-specs \
|
||||
--version=2.0.0
|
||||
```
|
||||
|
||||
## Generated OpenAPI Structure
|
||||
|
||||
For a service named `user-service`, the generated OpenAPI spec includes:
|
||||
|
||||
**Paths**:
|
||||
- `GET /users` - List users with pagination
|
||||
- `POST /users` - Create new user
|
||||
- `GET /users/{user_id}` - Get user by ID
|
||||
- `PUT /users/{user_id}` - Update user
|
||||
- `DELETE /users/{user_id}` - Delete user
|
||||
|
||||
**Schemas**:
|
||||
- `User` - Main resource schema
|
||||
- `UserCreate` - Creation payload schema
|
||||
- `UserUpdate` - Update payload schema
|
||||
- `Pagination` - Pagination metadata
|
||||
- `Problem` - RFC 7807 error schema
|
||||
|
||||
**Responses**:
|
||||
- `200` - Success (with X-Flow-ID header)
|
||||
- `201` - Created (with Location and X-Flow-ID headers)
|
||||
- `204` - No Content (for deletes)
|
||||
- `400` - Bad Request (application/problem+json)
|
||||
- `404` - Not Found (application/problem+json)
|
||||
- `409` - Conflict (application/problem+json)
|
||||
- `500` - Internal Error (application/problem+json)
|
||||
|
||||
**Security**:
|
||||
- Bearer token authentication (JWT)
|
||||
|
||||
**Required Metadata**:
|
||||
- `x-api-id` - Unique UUID
|
||||
- `x-audience` - Target audience (company-internal)
|
||||
- `contact` - Team contact information
|
||||
|
||||
## Resource Name Extraction
|
||||
|
||||
The skill automatically extracts resource names from service names:
|
||||
|
||||
| Service Name | Resource | Plural |
|
||||
|--------------|----------|--------|
|
||||
| `user-service` | user | users |
|
||||
| `order-api` | order | orders |
|
||||
| `payment-gateway` | payment | payments |
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
The skill automatically applies proper naming:
|
||||
|
||||
| Context | Convention | Example |
|
||||
|---------|------------|---------|
|
||||
| Paths | kebab-case | `/user-profiles` |
|
||||
| Properties | snake_case | `user_id` |
|
||||
| Schemas | TitleCase | `UserProfile` |
|
||||
| Operations | camelCase | `getUserById` |
|
||||
|
||||
## Integration with api.validate
|
||||
|
||||
The generated specs are designed to pass `api.validate` with zero errors:
|
||||
|
||||
```bash
|
||||
# Generate spec
|
||||
python skills/api.define/api_define.py user-service
|
||||
|
||||
# Validate (should pass)
|
||||
python skills/api.validate/api_validate.py specs/user-service.openapi.yaml zalando
|
||||
```
|
||||
|
||||
## Use in Workflows
|
||||
|
||||
```yaml
|
||||
# workflows/api_first_development.yaml
|
||||
steps:
|
||||
- skill: api.define
|
||||
args:
|
||||
- "user-service"
|
||||
- "openapi"
|
||||
- "--template=zalando"
|
||||
output: spec_path
|
||||
|
||||
- skill: api.validate
|
||||
args:
|
||||
- "{spec_path}"
|
||||
- "zalando"
|
||||
required: true
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
After generation, customize the spec:
|
||||
|
||||
1. **Add properties** to schemas:
|
||||
```yaml
|
||||
User:
|
||||
properties:
|
||||
user_id: ...
|
||||
email: # Add this
|
||||
type: string
|
||||
format: email
|
||||
```
|
||||
|
||||
2. **Add operations**:
|
||||
```yaml
|
||||
/users/search: # Add new endpoint
|
||||
post:
|
||||
summary: Search users
|
||||
```
|
||||
|
||||
3. **Modify metadata**:
|
||||
```yaml
|
||||
info:
|
||||
contact:
|
||||
name: Your Team Name # Update this
|
||||
email: team@company.com
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
### Success Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"spec_path": "specs/user-service.openapi.yaml",
|
||||
"spec_content": {...},
|
||||
"api_id": "d0184f38-b98d-11e7-9c56-68f728c1ba70",
|
||||
"template_used": "zalando",
|
||||
"service_name": "user-service"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **PyYAML**: Required for YAML handling
|
||||
```bash
|
||||
pip install pyyaml
|
||||
```
|
||||
|
||||
## Templates
|
||||
|
||||
| Template | Spec Type | Description |
|
||||
|----------|-----------|-------------|
|
||||
| `zalando` | OpenAPI | Zalando-compliant with all required fields |
|
||||
| `basic` | AsyncAPI | Basic event-driven API structure |
|
||||
|
||||
## See Also
|
||||
|
||||
- [api.validate](../api.validate/SKILL.md) - Validate generated specs
|
||||
- [hook.define](../hook.define/SKILL.md) - Set up automatic validation
|
||||
- [Betty Architecture](../../docs/betty-architecture.md) - Five-layer model
|
||||
- [API-Driven Development](../../docs/api-driven-development.md) - Complete guide
|
||||
|
||||
## Version
|
||||
|
||||
**0.1.0** - Initial implementation with Zalando template support
|
||||
1
skills/api.define/__init__.py
Normal file
1
skills/api.define/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Auto-generated package initializer for skills.
|
||||
446
skills/api.define/api_define.py
Executable file
446
skills/api.define/api_define.py
Executable file
@@ -0,0 +1,446 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Create OpenAPI and AsyncAPI specifications from templates.
|
||||
|
||||
This skill scaffolds API specifications following enterprise guidelines
|
||||
with proper structure and best practices built-in.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
import uuid
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
|
||||
# Add betty module to path
|
||||
|
||||
from betty.logging_utils import setup_logger
|
||||
from betty.errors import format_error_response, BettyError
|
||||
from betty.validation import validate_skill_name
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
def to_snake_case(text: str) -> str:
|
||||
"""Convert text to snake_case."""
|
||||
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', text)
|
||||
s2 = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1)
|
||||
return s2.lower().replace('-', '_').replace(' ', '_')
|
||||
|
||||
|
||||
def to_kebab_case(text: str) -> str:
|
||||
"""Convert text to kebab-case."""
|
||||
return to_snake_case(text).replace('_', '-')
|
||||
|
||||
|
||||
def to_title_case(text: str) -> str:
|
||||
"""Convert text to TitleCase."""
|
||||
return ''.join(word.capitalize() for word in re.split(r'[-_\s]', text))
|
||||
|
||||
|
||||
def pluralize(word: str) -> str:
|
||||
"""Simple pluralization (works for most common cases)."""
|
||||
if word.endswith('y'):
|
||||
return word[:-1] + 'ies'
|
||||
elif word.endswith('s'):
|
||||
return word + 'es'
|
||||
else:
|
||||
return word + 's'
|
||||
|
||||
|
||||
def load_template(template_name: str, spec_type: str) -> str:
|
||||
"""
|
||||
Load template file content.
|
||||
|
||||
Args:
|
||||
template_name: Template name (zalando, basic, minimal)
|
||||
spec_type: Specification type (openapi or asyncapi)
|
||||
|
||||
Returns:
|
||||
Template content as string
|
||||
|
||||
Raises:
|
||||
BettyError: If template not found
|
||||
"""
|
||||
template_file = Path(__file__).parent / "templates" / f"{spec_type}-{template_name}.yaml"
|
||||
|
||||
if not template_file.exists():
|
||||
raise BettyError(
|
||||
f"Template not found: {spec_type}-{template_name}.yaml. "
|
||||
f"Available templates in {template_file.parent}: "
|
||||
f"{', '.join([f.stem for f in template_file.parent.glob(f'{spec_type}-*.yaml')])}"
|
||||
)
|
||||
|
||||
try:
|
||||
with open(template_file, 'r') as f:
|
||||
content = f.read()
|
||||
logger.info(f"Loaded template: {template_file}")
|
||||
return content
|
||||
except Exception as e:
|
||||
raise BettyError(f"Failed to load template: {e}")
|
||||
|
||||
|
||||
def render_template(template: str, variables: Dict[str, str]) -> str:
|
||||
"""
|
||||
Render template with variables.
|
||||
|
||||
Args:
|
||||
template: Template string with {{variable}} placeholders
|
||||
variables: Variable values to substitute
|
||||
|
||||
Returns:
|
||||
Rendered template string
|
||||
"""
|
||||
result = template
|
||||
for key, value in variables.items():
|
||||
placeholder = f"{{{{{key}}}}}"
|
||||
result = result.replace(placeholder, str(value))
|
||||
|
||||
# Check for unrendered variables
|
||||
unrendered = re.findall(r'\{\{(\w+)\}\}', result)
|
||||
if unrendered:
|
||||
logger.warning(f"Unrendered template variables: {', '.join(set(unrendered))}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def extract_resource_name(service_name: str) -> str:
|
||||
"""
|
||||
Extract primary resource name from service name.
|
||||
|
||||
Examples:
|
||||
user-service -> user
|
||||
order-api -> order
|
||||
payment-gateway -> payment
|
||||
"""
|
||||
# Remove common suffixes
|
||||
for suffix in ['-service', '-api', '-gateway', '-manager']:
|
||||
if service_name.endswith(suffix):
|
||||
return service_name[:-len(suffix)]
|
||||
|
||||
return service_name
|
||||
|
||||
|
||||
def generate_openapi_spec(
|
||||
service_name: str,
|
||||
template_name: str = "zalando",
|
||||
version: str = "1.0.0",
|
||||
output_dir: str = "specs"
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate OpenAPI specification from template.
|
||||
|
||||
Args:
|
||||
service_name: Service/API name
|
||||
template_name: Template to use
|
||||
version: API version
|
||||
output_dir: Output directory
|
||||
|
||||
Returns:
|
||||
Result dictionary with spec path and content
|
||||
"""
|
||||
# Generate API ID
|
||||
api_id = str(uuid.uuid4())
|
||||
|
||||
# Extract resource name
|
||||
resource_name = extract_resource_name(service_name)
|
||||
|
||||
# Generate template variables
|
||||
variables = {
|
||||
"service_name": to_kebab_case(service_name),
|
||||
"service_title": to_title_case(service_name),
|
||||
"version": version,
|
||||
"description": f"RESTful API for {service_name.replace('-', ' ')} management",
|
||||
"team_name": "Platform Team",
|
||||
"team_email": "platform@company.com",
|
||||
"api_id": api_id,
|
||||
"audience": "company-internal",
|
||||
"resource_singular": to_snake_case(resource_name),
|
||||
"resource_plural": pluralize(to_snake_case(resource_name)),
|
||||
"resource_title": to_title_case(resource_name),
|
||||
"resource_schema": to_title_case(resource_name)
|
||||
}
|
||||
|
||||
logger.info(f"Generated template variables: {variables}")
|
||||
|
||||
# Load and render template
|
||||
template = load_template(template_name, "openapi")
|
||||
spec_content = render_template(template, variables)
|
||||
|
||||
# Parse to validate YAML
|
||||
try:
|
||||
import yaml
|
||||
spec_dict = yaml.safe_load(spec_content)
|
||||
except Exception as e:
|
||||
raise BettyError(f"Failed to parse generated spec: {e}")
|
||||
|
||||
# Create output directory
|
||||
output_path = Path(output_dir)
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write specification file
|
||||
spec_filename = f"{to_kebab_case(service_name)}.openapi.yaml"
|
||||
spec_path = output_path / spec_filename
|
||||
|
||||
with open(spec_path, 'w') as f:
|
||||
f.write(spec_content)
|
||||
|
||||
logger.info(f"Generated OpenAPI spec: {spec_path}")
|
||||
|
||||
return {
|
||||
"spec_path": str(spec_path),
|
||||
"spec_content": spec_dict,
|
||||
"api_id": api_id,
|
||||
"template_used": template_name,
|
||||
"service_name": to_kebab_case(service_name)
|
||||
}
|
||||
|
||||
|
||||
def generate_asyncapi_spec(
|
||||
service_name: str,
|
||||
template_name: str = "basic",
|
||||
version: str = "1.0.0",
|
||||
output_dir: str = "specs"
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate AsyncAPI specification from template.
|
||||
|
||||
Args:
|
||||
service_name: Service/API name
|
||||
template_name: Template to use
|
||||
version: API version
|
||||
output_dir: Output directory
|
||||
|
||||
Returns:
|
||||
Result dictionary with spec path and content
|
||||
"""
|
||||
# Basic AsyncAPI template (inline for now)
|
||||
resource_name = extract_resource_name(service_name)
|
||||
|
||||
asyncapi_template = f"""asyncapi: 3.0.0
|
||||
|
||||
info:
|
||||
title: {to_title_case(service_name)} Events
|
||||
version: {version}
|
||||
description: Event-driven API for {service_name.replace('-', ' ')} lifecycle notifications
|
||||
|
||||
servers:
|
||||
production:
|
||||
host: kafka.company.com:9092
|
||||
protocol: kafka
|
||||
description: Production Kafka cluster
|
||||
|
||||
channels:
|
||||
{to_snake_case(resource_name)}.created:
|
||||
address: {to_snake_case(resource_name)}.created.v1
|
||||
messages:
|
||||
{to_title_case(resource_name)}Created:
|
||||
$ref: '#/components/messages/{to_title_case(resource_name)}Created'
|
||||
|
||||
{to_snake_case(resource_name)}.updated:
|
||||
address: {to_snake_case(resource_name)}.updated.v1
|
||||
messages:
|
||||
{to_title_case(resource_name)}Updated:
|
||||
$ref: '#/components/messages/{to_title_case(resource_name)}Updated'
|
||||
|
||||
{to_snake_case(resource_name)}.deleted:
|
||||
address: {to_snake_case(resource_name)}.deleted.v1
|
||||
messages:
|
||||
{to_title_case(resource_name)}Deleted:
|
||||
$ref: '#/components/messages/{to_title_case(resource_name)}Deleted'
|
||||
|
||||
operations:
|
||||
publish{to_title_case(resource_name)}Created:
|
||||
action: send
|
||||
channel:
|
||||
$ref: '#/channels/{to_snake_case(resource_name)}.created'
|
||||
|
||||
subscribe{to_title_case(resource_name)}Created:
|
||||
action: receive
|
||||
channel:
|
||||
$ref: '#/channels/{to_snake_case(resource_name)}.created'
|
||||
|
||||
components:
|
||||
messages:
|
||||
{to_title_case(resource_name)}Created:
|
||||
name: {to_title_case(resource_name)}Created
|
||||
title: {to_title_case(resource_name)} Created Event
|
||||
contentType: application/json
|
||||
payload:
|
||||
$ref: '#/components/schemas/{to_title_case(resource_name)}CreatedPayload'
|
||||
|
||||
{to_title_case(resource_name)}Updated:
|
||||
name: {to_title_case(resource_name)}Updated
|
||||
title: {to_title_case(resource_name)} Updated Event
|
||||
contentType: application/json
|
||||
payload:
|
||||
$ref: '#/components/schemas/{to_title_case(resource_name)}UpdatedPayload'
|
||||
|
||||
{to_title_case(resource_name)}Deleted:
|
||||
name: {to_title_case(resource_name)}Deleted
|
||||
title: {to_title_case(resource_name)} Deleted Event
|
||||
contentType: application/json
|
||||
payload:
|
||||
$ref: '#/components/schemas/{to_title_case(resource_name)}DeletedPayload'
|
||||
|
||||
schemas:
|
||||
{to_title_case(resource_name)}CreatedPayload:
|
||||
type: object
|
||||
required: [event_id, {to_snake_case(resource_name)}_id, occurred_at]
|
||||
properties:
|
||||
event_id:
|
||||
type: string
|
||||
format: uuid
|
||||
{to_snake_case(resource_name)}_id:
|
||||
type: string
|
||||
format: uuid
|
||||
occurred_at:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
{to_title_case(resource_name)}UpdatedPayload:
|
||||
type: object
|
||||
required: [event_id, {to_snake_case(resource_name)}_id, occurred_at, changes]
|
||||
properties:
|
||||
event_id:
|
||||
type: string
|
||||
format: uuid
|
||||
{to_snake_case(resource_name)}_id:
|
||||
type: string
|
||||
format: uuid
|
||||
changes:
|
||||
type: object
|
||||
occurred_at:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
{to_title_case(resource_name)}DeletedPayload:
|
||||
type: object
|
||||
required: [event_id, {to_snake_case(resource_name)}_id, occurred_at]
|
||||
properties:
|
||||
event_id:
|
||||
type: string
|
||||
format: uuid
|
||||
{to_snake_case(resource_name)}_id:
|
||||
type: string
|
||||
format: uuid
|
||||
occurred_at:
|
||||
type: string
|
||||
format: date-time
|
||||
"""
|
||||
|
||||
# Parse to validate YAML
|
||||
try:
|
||||
import yaml
|
||||
spec_dict = yaml.safe_load(asyncapi_template)
|
||||
except Exception as e:
|
||||
raise BettyError(f"Failed to parse generated spec: {e}")
|
||||
|
||||
# Create output directory
|
||||
output_path = Path(output_dir)
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write specification file
|
||||
spec_filename = f"{to_kebab_case(service_name)}.asyncapi.yaml"
|
||||
spec_path = output_path / spec_filename
|
||||
|
||||
with open(spec_path, 'w') as f:
|
||||
f.write(asyncapi_template)
|
||||
|
||||
logger.info(f"Generated AsyncAPI spec: {spec_path}")
|
||||
|
||||
return {
|
||||
"spec_path": str(spec_path),
|
||||
"spec_content": spec_dict,
|
||||
"template_used": template_name,
|
||||
"service_name": to_kebab_case(service_name)
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Create OpenAPI and AsyncAPI specifications from templates"
|
||||
)
|
||||
parser.add_argument(
|
||||
"service_name",
|
||||
type=str,
|
||||
help="Name of the service/API (e.g., user-service, order-api)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"spec_type",
|
||||
type=str,
|
||||
nargs="?",
|
||||
default="openapi",
|
||||
choices=["openapi", "asyncapi"],
|
||||
help="Type of specification (default: openapi)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--template",
|
||||
type=str,
|
||||
default="zalando",
|
||||
help="Template to use (default: zalando for OpenAPI, basic for AsyncAPI)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
type=str,
|
||||
default="specs",
|
||||
help="Output directory (default: specs)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
type=str,
|
||||
default="1.0.0",
|
||||
help="API version (default: 1.0.0)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
# Check if PyYAML is installed
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
raise BettyError(
|
||||
"PyYAML is required for api.define. Install with: pip install pyyaml"
|
||||
)
|
||||
|
||||
# Generate specification
|
||||
logger.info(
|
||||
f"Generating {args.spec_type.upper()} spec for '{args.service_name}' "
|
||||
f"using template '{args.template}'"
|
||||
)
|
||||
|
||||
if args.spec_type == "openapi":
|
||||
result = generate_openapi_spec(
|
||||
service_name=args.service_name,
|
||||
template_name=args.template,
|
||||
version=args.version,
|
||||
output_dir=args.output_dir
|
||||
)
|
||||
elif args.spec_type == "asyncapi":
|
||||
result = generate_asyncapi_spec(
|
||||
service_name=args.service_name,
|
||||
template_name=args.template,
|
||||
version=args.version,
|
||||
output_dir=args.output_dir
|
||||
)
|
||||
else:
|
||||
raise BettyError(f"Unsupported spec type: {args.spec_type}")
|
||||
|
||||
# Return structured result
|
||||
output = {
|
||||
"status": "success",
|
||||
"data": result
|
||||
}
|
||||
print(json.dumps(output, indent=2))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate specification: {e}")
|
||||
print(json.dumps(format_error_response(e), indent=2))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
57
skills/api.define/skill.yaml
Normal file
57
skills/api.define/skill.yaml
Normal file
@@ -0,0 +1,57 @@
|
||||
name: api.define
|
||||
version: 0.1.0
|
||||
description: Create OpenAPI and AsyncAPI specifications from templates
|
||||
|
||||
inputs:
|
||||
- name: service_name
|
||||
type: string
|
||||
required: true
|
||||
description: Name of the service/API (e.g., user-service, order-api)
|
||||
|
||||
- name: spec_type
|
||||
type: string
|
||||
required: false
|
||||
default: openapi
|
||||
description: Type of specification (openapi or asyncapi)
|
||||
|
||||
- name: template
|
||||
type: string
|
||||
required: false
|
||||
default: zalando
|
||||
description: Template to use (zalando, basic, minimal)
|
||||
|
||||
- name: output_dir
|
||||
type: string
|
||||
required: false
|
||||
default: specs
|
||||
description: Output directory for generated specification
|
||||
|
||||
- name: version
|
||||
type: string
|
||||
required: false
|
||||
default: 1.0.0
|
||||
description: API version
|
||||
|
||||
outputs:
|
||||
- name: spec_path
|
||||
type: string
|
||||
description: Path to generated specification file
|
||||
|
||||
- name: spec_content
|
||||
type: object
|
||||
description: Generated specification content
|
||||
|
||||
dependencies:
|
||||
- context.schema
|
||||
|
||||
entrypoints:
|
||||
- command: /skill/api/define
|
||||
handler: api_define.py
|
||||
runtime: python
|
||||
permissions:
|
||||
- filesystem:read
|
||||
- filesystem:write
|
||||
|
||||
status: active
|
||||
|
||||
tags: [api, openapi, asyncapi, scaffolding, zalando]
|
||||
1
skills/api.define/templates/__init__.py
Normal file
1
skills/api.define/templates/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Auto-generated package initializer for skills.
|
||||
303
skills/api.define/templates/openapi-zalando.yaml
Normal file
303
skills/api.define/templates/openapi-zalando.yaml
Normal file
@@ -0,0 +1,303 @@
|
||||
openapi: 3.1.0
|
||||
|
||||
info:
|
||||
title: {{service_title}}
|
||||
version: {{version}}
|
||||
description: {{description}}
|
||||
contact:
|
||||
name: {{team_name}}
|
||||
email: {{team_email}}
|
||||
x-api-id: {{api_id}}
|
||||
x-audience: {{audience}}
|
||||
|
||||
servers:
|
||||
- url: https://api.company.com/{{service_name}}/v1
|
||||
description: Production
|
||||
|
||||
paths:
|
||||
/{{resource_plural}}:
|
||||
get:
|
||||
summary: List {{resource_plural}}
|
||||
operationId: list{{resource_title}}
|
||||
tags: [{{resource_title}}]
|
||||
parameters:
|
||||
- name: limit
|
||||
in: query
|
||||
description: Maximum number of items to return
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 20
|
||||
- name: offset
|
||||
in: query
|
||||
description: Number of items to skip
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
default: 0
|
||||
responses:
|
||||
'200':
|
||||
description: List of {{resource_plural}}
|
||||
headers:
|
||||
X-Flow-ID:
|
||||
description: Request flow ID for tracing
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [{{resource_plural}}, pagination]
|
||||
properties:
|
||||
{{resource_plural}}:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/{{resource_schema}}'
|
||||
pagination:
|
||||
$ref: '#/components/schemas/Pagination'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalError'
|
||||
|
||||
post:
|
||||
summary: Create a new {{resource_singular}}
|
||||
operationId: create{{resource_title}}
|
||||
tags: [{{resource_title}}]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/{{resource_schema}}Create'
|
||||
responses:
|
||||
'201':
|
||||
description: {{resource_title}} created successfully
|
||||
headers:
|
||||
Location:
|
||||
description: URL of the created resource
|
||||
schema:
|
||||
type: string
|
||||
format: uri
|
||||
X-Flow-ID:
|
||||
description: Request flow ID for tracing
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/{{resource_schema}}'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'409':
|
||||
$ref: '#/components/responses/Conflict'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalError'
|
||||
|
||||
/{{resource_plural}}/{{{resource_singular}}_id}:
|
||||
parameters:
|
||||
- name: {{resource_singular}}_id
|
||||
in: path
|
||||
required: true
|
||||
description: Unique identifier of the {{resource_singular}}
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
|
||||
get:
|
||||
summary: Get {{resource_singular}} by ID
|
||||
operationId: get{{resource_title}}ById
|
||||
tags: [{{resource_title}}]
|
||||
responses:
|
||||
'200':
|
||||
description: {{resource_title}} details
|
||||
headers:
|
||||
X-Flow-ID:
|
||||
description: Request flow ID for tracing
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/{{resource_schema}}'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalError'
|
||||
|
||||
put:
|
||||
summary: Update {{resource_singular}}
|
||||
operationId: update{{resource_title}}
|
||||
tags: [{{resource_title}}]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/{{resource_schema}}Update'
|
||||
responses:
|
||||
'200':
|
||||
description: {{resource_title}} updated successfully
|
||||
headers:
|
||||
X-Flow-ID:
|
||||
description: Request flow ID for tracing
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/{{resource_schema}}'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalError'
|
||||
|
||||
delete:
|
||||
summary: Delete {{resource_singular}}
|
||||
operationId: delete{{resource_title}}
|
||||
tags: [{{resource_title}}]
|
||||
responses:
|
||||
'204':
|
||||
description: {{resource_title}} deleted successfully
|
||||
headers:
|
||||
X-Flow-ID:
|
||||
description: Request flow ID for tracing
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalError'
|
||||
|
||||
components:
|
||||
schemas:
|
||||
{{resource_schema}}:
|
||||
type: object
|
||||
required: [{{resource_singular}}_id, created_at]
|
||||
properties:
|
||||
{{resource_singular}}_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Unique identifier
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Creation timestamp
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Last update timestamp
|
||||
|
||||
{{resource_schema}}Create:
|
||||
type: object
|
||||
required: []
|
||||
properties:
|
||||
# Add creation-specific fields here
|
||||
|
||||
{{resource_schema}}Update:
|
||||
type: object
|
||||
properties:
|
||||
# Add update-specific fields here
|
||||
|
||||
Pagination:
|
||||
type: object
|
||||
required: [limit, offset, total]
|
||||
properties:
|
||||
limit:
|
||||
type: integer
|
||||
description: Number of items per page
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items skipped
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of items available
|
||||
|
||||
Problem:
|
||||
type: object
|
||||
required: [type, title, status]
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
format: uri
|
||||
description: URI reference identifying the problem type
|
||||
title:
|
||||
type: string
|
||||
description: Short, human-readable summary
|
||||
status:
|
||||
type: integer
|
||||
description: HTTP status code
|
||||
detail:
|
||||
type: string
|
||||
description: Human-readable explanation
|
||||
instance:
|
||||
type: string
|
||||
format: uri
|
||||
description: URI reference identifying the specific occurrence
|
||||
|
||||
responses:
|
||||
BadRequest:
|
||||
description: Bad request - invalid parameters or malformed request
|
||||
content:
|
||||
application/problem+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Problem'
|
||||
example:
|
||||
type: https://api.company.com/problems/bad-request
|
||||
title: Bad Request
|
||||
status: 400
|
||||
detail: "Invalid query parameter 'limit': must be between 1 and 100"
|
||||
|
||||
NotFound:
|
||||
description: Resource not found
|
||||
content:
|
||||
application/problem+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Problem'
|
||||
example:
|
||||
type: https://api.company.com/problems/not-found
|
||||
title: Not Found
|
||||
status: 404
|
||||
detail: {{resource_title}} with the specified ID was not found
|
||||
|
||||
Conflict:
|
||||
description: Conflict - resource already exists or state conflict
|
||||
content:
|
||||
application/problem+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Problem'
|
||||
example:
|
||||
type: https://api.company.com/problems/conflict
|
||||
title: Conflict
|
||||
status: 409
|
||||
detail: {{resource_title}} with this identifier already exists
|
||||
|
||||
InternalError:
|
||||
description: Internal server error
|
||||
content:
|
||||
application/problem+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Problem'
|
||||
example:
|
||||
type: https://api.company.com/problems/internal-error
|
||||
title: Internal Server Error
|
||||
status: 500
|
||||
detail: An unexpected error occurred while processing the request
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: JWT-based authentication
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
Reference in New Issue
Block a user