Merge branch 'development' into development_actions007

This commit is contained in:
Timur Gordon
2025-03-17 00:39:38 +01:00
210 changed files with 14100 additions and 2744 deletions

View File

@@ -0,0 +1,3 @@
methods.v
pet_store_actor
docs

View File

@@ -0,0 +1,9 @@
# Actor Generation Examples
## `generate_methods.vsh`
This example generates actor method prototypes from an actor specification.
## `generate_actor_module.vsh`
This example generates an entire actor module from an actor specification with the support for the specified interfaces.

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.baobab.generator
import freeflowuniverse.herolib.baobab.specification
import freeflowuniverse.herolib.schemas.openrpc
import os
const example_dir = os.dir(@FILE)
const openrpc_spec_path = os.join_path(example_dir, 'openrpc.json')
// the actor specification obtained from the OpenRPC Specification
openrpc_spec := openrpc.new(path: openrpc_spec_path)!
actor_spec := specification.from_openrpc(openrpc_spec)!
actor_module := generator.generate_actor_module(
actor_spec,
interfaces: [.openrpc]
)!
actor_module.write(example_dir,
format: true
overwrite: true
)!

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.baobab.generator
import freeflowuniverse.herolib.baobab.specification
import freeflowuniverse.herolib.schemas.openrpc
import os
const example_dir = os.dir(@FILE)
const openrpc_spec_path = os.join_path(example_dir, 'openrpc.json')
// the actor specification obtained from the OpenRPC Specification
openrpc_spec := openrpc.new(path: openrpc_spec_path)!
actor_spec := specification.from_openrpc(openrpc_spec)!
methods_file := generator.generate_methods_file(actor_spec)!
methods_file.write(example_dir,
format: true
overwrite: true
)!

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.baobab.generator
import freeflowuniverse.herolib.baobab.specification
import freeflowuniverse.herolib.schemas.openrpc
import os
const example_dir = os.dir(@FILE)
const openrpc_spec_path = os.join_path(example_dir, 'openrpc.json')
// the actor specification obtained from the OpenRPC Specification
openrpc_spec_ := openrpc.new(path: openrpc_spec_path)!
actor_spec := specification.from_openrpc(openrpc_spec_)!
openrpc_spec := actor_spec.to_openrpc()
openrpc_file := generator.generate_openrpc_file(openrpc_spec)!
openrpc_file.write(os.join_path(example_dir,'docs'),
overwrite: true
)!

View File

@@ -0,0 +1,132 @@
{
"openrpc": "1.0.0",
"info": {
"title": "PetStore",
"version": "1.0.0"
},
"methods": [
{
"name": "GetPets",
"description": "finds pets in the system that the user has access to by tags and within a limit",
"params": [
{
"name": "tags",
"description": "tags to filter by",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
},
{
"name": "limit",
"description": "maximum number of results to return",
"schema": {
"type": "integer"
}
}
],
"result": {
"name": "pet_list",
"description": "all pets from the system, that mathes the tags",
"schema": {
"$ref": "#\/components\/schemas\/Pet"
}
}
},
{
"name": "CreatePet",
"description": "creates a new pet in the store. Duplicates are allowed.",
"params": [
{
"name": "new_pet",
"description": "Pet to add to the store.",
"schema": {
"$ref": "#\/components\/schemas\/NewPet"
}
}
],
"result": {
"name": "pet",
"description": "the newly created pet",
"schema": {
"$ref": "#\/components\/schemas\/Pet"
}
}
},
{
"name": "GetPetById",
"description": "gets a pet based on a single ID, if the user has access to the pet",
"params": [
{
"name": "id",
"description": "ID of pet to fetch",
"schema": {
"type": "integer"
}
}
],
"result": {
"name": "pet",
"description": "pet response",
"schema": {
"$ref": "#\/components\/schemas\/Pet"
}
}
},
{
"name": "DeletePetById",
"description": "deletes a single pet based on the ID supplied",
"params": [
{
"name": "id",
"description": "ID of pet to delete",
"schema": {
"type": "integer"
}
}
],
"result": {
"name": "pet",
"description": "pet deleted",
"schema": {
"type": "null"
}
}
}
],
"components": {
"schemas": {
"NewPet": {
"title": "NewPet",
"properties": {
"name": {
"type": "string"
},
"tag": {
"type": "string"
}
}
},
"Pet": {
"title": "Pet",
"description": "a pet struct that represents a pet",
"properties": {
"name": {
"description": "name of the pet",
"type": "string"
},
"tag": {
"description": "a tag of the pet, helps finding pet",
"type": "string"
},
"id": {
"description": "unique indentifier",
"type": "integer"
}
}
}
}
}
}

View File

@@ -0,0 +1,3 @@
merchant
profiler
farmer

View File

@@ -0,0 +1,344 @@
{
"openapi": "3.0.1",
"info": {
"title": "Farmer",
"description": "API for managing farms and nodes, tracking rewards, capacity, and location.",
"version": "1.0.0"
},
"servers": [
{
"url": "http://localhost:8080",
"description": "Local development server"
}
],
"components": {
"schemas": {
"Farm": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"example": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
},
"name": {
"type": "string",
"example": "Amsterdam Data Center"
},
"description": {
"type": "string",
"example": "Enterprise-grade data center with renewable energy focus"
},
"owner": {
"type": "string",
"example": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e"
}
},
"required": [
"id",
"name",
"owner"
]
},
"Node": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"example": "n47ac10b-58cc-4372-a567-0e02b2c3d479"
},
"description": {
"type": "string",
"example": "High-performance GPU compute node with 4x NVIDIA A100"
},
"farm_id": {
"type": "string",
"format": "uuid",
"example": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
},
"location": {
"$ref": "#/components/schemas/Location"
},
"capacity": {
"$ref": "#/components/schemas/Capacity"
},
"grid_version": {
"type": "string",
"example": "3.16.2"
},
"reward": {
"$ref": "#/components/schemas/Reward"
}
},
"required": [
"id",
"description",
"farm_id",
"location",
"capacity",
"reward"
]
},
"Location": {
"type": "object",
"properties": {
"coordinates": {
"type": "string",
"example": "52.3740, 4.8897"
},
"continent": {
"type": "string",
"example": "Europe"
},
"country": {
"type": "string",
"example": "Netherlands"
}
},
"required": [
"coordinates",
"continent",
"country"
]
},
"Capacity": {
"type": "object",
"properties": {
"cpu": {
"type": "integer",
"example": 128
},
"memory_gb": {
"type": "integer",
"example": 1024
},
"storage_tb": {
"type": "integer",
"example": 100
}
},
"required": [
"cpu",
"memory_gb",
"storage_tb"
]
},
"Reward": {
"type": "object",
"properties": {
"reward_promised": {
"type": "number",
"format": "double",
"example": 25000.50
},
"reward_given": {
"type": "number",
"format": "double",
"example": 12500.25
},
"duration_months": {
"type": "integer",
"example": 36
}
},
"required": [
"reward_promised",
"reward_given",
"duration_months"
]
},
"NodeStats": {
"type": "object",
"properties": {
"node_id": {
"type": "integer",
"format": "uint32",
"example": "42"
},
"uptime_hours": {
"type": "integer",
"example": 8760
},
"bandwidth_gb": {
"type": "integer",
"example": 25000
}
},
"required": [
"node_id",
"uptime_hours",
"bandwidth_gb"
]
}
}
},
"paths": {
"/farms": {
"get": {
"summary": "List all farms",
"operationId": "getFarms",
"responses": {
"200": {
"description": "List of farms",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Farm"
}
},
"example": [
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"name": "Amsterdam Data Center",
"description": "Enterprise-grade data center with renewable energy focus",
"owner": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e"
},
{
"id": "d47ac10b-58cc-4372-a567-0e02b2c3d480",
"name": "Dubai Compute Hub",
"description": "High-density compute farm with advanced cooling",
"owner": "0x842d35Cc6634C0532925a3b844Bc454e4438f55f"
}
]
}
}
}
}
}
},
"/farms/{farmId}/nodes": {
"get": {
"summary": "List nodes in a farm",
"operationId": "getNodesByFarm",
"parameters": [
{
"name": "farmId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
},
"example": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}
],
"responses": {
"200": {
"description": "List of nodes in the farm",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Node"
}
},
"example": [
{
"id": "n47ac10b-58cc-4372-a567-0e02b2c3d479",
"description": "High-performance GPU compute node with 4x NVIDIA A100",
"farm_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"location": {
"coordinates": "52.3740, 4.8897",
"continent": "Europe",
"country": "Netherlands"
},
"capacity": {
"cpu": 128,
"memory_gb": 1024,
"storage_tb": 100
},
"grid_version": "3.16.2",
"reward": {
"reward_promised": 25000.50,
"reward_given": 12500.25,
"duration_months": 36
}
}
]
}
}
},
"404": {
"description": "Farm not found",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"example": 404
},
"message": {
"type": "string",
"example": "Farm with ID f47ac10b-58cc-4372-a567-0e02b2c3d479 not found"
}
}
}
}
}
}
}
}
},
"/nodes/{nodeId}/stats": {
"get": {
"summary": "Get node statistics",
"operationId": "getNodeStats",
"parameters": [
{
"name": "nodeId",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "uint32"
},
"example": "42"
}
],
"responses": {
"200": {
"description": "Node statistics",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NodeStats"
},
"example": {
"node_id": "42",
"uptime_hours": 8760,
"bandwidth_gb": 25000
}
}
}
},
"404": {
"description": "Node not found",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"example": 404
},
"message": {
"type": "string",
"example": "Node with ID n47ac10b-58cc-4372-a567-0e02b2c3d479 not found"
}
}
}
}
}
}
}
}
}
}
}

Binary file not shown.

View File

@@ -0,0 +1,25 @@
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.baobab.generator
import freeflowuniverse.herolib.baobab.specification
import freeflowuniverse.herolib.schemas.openapi
import os
const example_dir = os.dir(@FILE)
const specs = ['merchant', 'profiler', 'farmer']
for spec in specs {
openapi_spec_path := os.join_path(example_dir, '${spec}.json')
openapi_spec := openapi.new(path: openapi_spec_path, process: true)!
actor_spec := specification.from_openapi(openapi_spec)!
actor_module := generator.generate_actor_folder(
actor_spec,
interfaces: [.openapi, .http]
)!
actor_module.write(example_dir,
format: true
overwrite: true
compile: false
)!
}

View File

@@ -0,0 +1,997 @@
{
"openapi": "3.0.1",
"info": {
"title": "Merchant",
"description": "API for e-commerce operations including stores, products, and orders",
"version": "1.0.0"
},
"servers": [{
"url": "http://localhost:8080",
"description": "Local development server"
},{
"url": "http://localhost:8080/openapi/example",
"description": "Local example server"
}],
"components": {
"schemas": {
"Store": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"example": "123e4567-e89b-12d3-a456-426614174000"
},
"name": {
"type": "string",
"example": "Tech Gadgets Store"
},
"description": {
"type": "string",
"example": "Premium electronics and gadgets retailer"
},
"contact": {
"type": "string",
"example": "contact@techgadgets.com"
},
"active": {
"type": "boolean",
"example": true
}
},
"required": [
"id",
"name",
"contact",
"active"
]
},
"ProductComponentTemplate": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"example": "123e4567-e89b-12d3-a456-426614174001"
},
"name": {
"type": "string",
"example": "4K Display Panel"
},
"description": {
"type": "string",
"example": "55-inch 4K UHD Display Panel"
},
"specs": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"example": {
"resolution": "3840x2160",
"refreshRate": "120Hz",
"panel_type": "OLED"
}
},
"price": {
"type": "number",
"format": "double",
"example": 599.99
},
"currency": {
"type": "string",
"pattern": "^[A-Z]{3}$",
"example": "USD"
}
},
"required": [
"id",
"name",
"price",
"currency"
]
},
"ProductTemplate": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"example": "123e4567-e89b-12d3-a456-426614174002"
},
"name": {
"type": "string",
"example": "Smart TV 55-inch"
},
"description": {
"type": "string",
"example": "55-inch Smart TV with 4K Display"
},
"components": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ProductComponentTemplate"
},
"example": [
{
"id": "123e4567-e89b-12d3-a456-426614174001",
"name": "4K Display Panel",
"description": "55-inch 4K UHD Display Panel",
"specs": {
"resolution": "3840x2160",
"refreshRate": "120Hz"
},
"price": 599.99,
"currency": "USD"
}
]
},
"store_id": {
"type": "string",
"format": "uuid",
"example": "123e4567-e89b-12d3-a456-426614174000"
},
"category": {
"type": "string",
"example": "Electronics"
},
"active": {
"type": "boolean",
"example": true
}
},
"required": [
"id",
"name",
"components",
"store_id",
"active"
]
},
"Product": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"example": "123e4567-e89b-12d3-a456-426614174003"
},
"template_id": {
"type": "string",
"format": "uuid",
"example": "123e4567-e89b-12d3-a456-426614174002"
},
"name": {
"type": "string",
"example": "Smart TV 55-inch"
},
"description": {
"type": "string",
"example": "55-inch Smart TV with 4K Display"
},
"price": {
"type": "number",
"format": "double",
"example": 899.99
},
"currency": {
"type": "string",
"pattern": "^[A-Z]{3}$",
"example": "USD"
},
"store_id": {
"type": "string",
"format": "uuid",
"example": "123e4567-e89b-12d3-a456-426614174000"
},
"stock_quantity": {
"type": "integer",
"minimum": 0,
"example": 50
},
"available": {
"type": "boolean",
"example": true
}
},
"required": [
"id",
"template_id",
"name",
"price",
"currency",
"store_id",
"stock_quantity",
"available"
]
},
"OrderItem": {
"type": "object",
"properties": {
"product_id": {
"type": "string",
"format": "uuid",
"example": "123e4567-e89b-12d3-a456-426614174003"
},
"quantity": {
"type": "integer",
"minimum": 1,
"example": 2
},
"price": {
"type": "number",
"format": "double",
"example": 899.99
},
"currency": {
"type": "string",
"pattern": "^[A-Z]{3}$",
"example": "USD"
}
},
"required": [
"product_id",
"quantity",
"price",
"currency"
]
},
"Order": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"example": "123e4567-e89b-12d3-a456-426614174004"
},
"customer_id": {
"type": "string",
"format": "uuid",
"example": "123e4567-e89b-12d3-a456-426614174005"
},
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/OrderItem"
},
"example": [
{
"product_id": "123e4567-e89b-12d3-a456-426614174003",
"quantity": 2,
"price": 899.99,
"currency": "USD"
}
]
},
"total_amount": {
"type": "number",
"format": "double",
"example": 1799.98
},
"currency": {
"type": "string",
"pattern": "^[A-Z]{3}$",
"example": "USD"
},
"status": {
"type": "string",
"enum": [
"pending",
"confirmed",
"shipped",
"delivered"
],
"example": "pending"
},
"created_at": {
"type": "string",
"format": "date-time",
"example": "2024-02-10T10:30:00Z"
},
"updated_at": {
"type": "string",
"format": "date-time",
"example": "2024-02-10T10:30:00Z"
}
},
"required": [
"id",
"customer_id",
"items",
"total_amount",
"currency",
"status",
"created_at",
"updated_at"
]
},
"Error": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"example": 404
},
"message": {
"type": "string",
"example": "Resource not found"
}
},
"required": [
"code",
"message"
]
}
}
},
"paths": {
"/stores": {
"post": {
"summary": "Create a new store",
"operationId": "createStore",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "Tech Gadgets Store"
},
"description": {
"type": "string",
"example": "Premium electronics and gadgets retailer"
},
"contact": {
"type": "string",
"example": "contact@techgadgets.com"
}
},
"required": [
"name",
"contact"
]
},
"examples": {
"newStore": {
"summary": "Create a new electronics store",
"value": {
"name": "Tech Gadgets Store",
"description": "Premium electronics and gadgets retailer",
"contact": "contact@techgadgets.com"
}
}
}
}
}
},
"responses": {
"201": {
"description": "Store created successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Store"
},
"example": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"name": "Tech Gadgets Store",
"description": "Premium electronics and gadgets retailer",
"contact": "contact@techgadgets.com",
"active": true
}
}
}
},
"400": {
"description": "Invalid input",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"code": 400,
"message": "Invalid store data provided"
}
}
}
}
}
}
},
"/products/templates/components": {
"post": {
"summary": "Create a new product component template",
"operationId": "createProductComponentTemplate",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"specs": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"price": {
"type": "number"
},
"currency": {
"type": "string"
}
},
"required": [
"name",
"price",
"currency"
]
},
"examples": {
"displayPanel": {
"summary": "Create a display panel component",
"value": {
"name": "4K Display Panel",
"description": "55-inch 4K UHD Display Panel",
"specs": {
"resolution": "3840x2160",
"refreshRate": "120Hz",
"panel_type": "OLED"
},
"price": 599.99,
"currency": "USD"
}
}
}
}
}
},
"responses": {
"201": {
"description": "Component template created successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductComponentTemplate"
},
"example": {
"id": "123e4567-e89b-12d3-a456-426614174001",
"name": "4K Display Panel",
"description": "55-inch 4K UHD Display Panel",
"specs": {
"resolution": "3840x2160",
"refreshRate": "120Hz",
"panel_type": "OLED"
},
"price": 599.99,
"currency": "USD"
}
}
}
},
"400": {
"description": "Invalid input",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"code": 400,
"message": "Invalid component template data"
}
}
}
}
}
}
},
"/products/templates": {
"post": {
"summary": "Create a new product template",
"operationId": "createProductTemplate",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"components": {
"type": "array",
"items": {
"type": "string",
"format": "uuid"
}
},
"store_id": {
"type": "string",
"format": "uuid"
},
"category": {
"type": "string"
}
},
"required": [
"name",
"components",
"store_id"
]
},
"examples": {
"smartTV": {
"summary": "Create a Smart TV template",
"value": {
"name": "Smart TV 55-inch",
"description": "55-inch Smart TV with 4K Display",
"components": [
"123e4567-e89b-12d3-a456-426614174001"
],
"store_id": "123e4567-e89b-12d3-a456-426614174000",
"category": "Electronics"
}
}
}
}
}
},
"responses": {
"201": {
"description": "Product template created successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductTemplate"
},
"example": {
"id": "123e4567-e89b-12d3-a456-426614174002",
"name": "Smart TV 55-inch",
"description": "55-inch Smart TV with 4K Display",
"components": [
{
"id": "123e4567-e89b-12d3-a456-426614174001",
"name": "4K Display Panel",
"description": "55-inch 4K UHD Display Panel",
"specs": {
"resolution": "3840x2160",
"refreshRate": "120Hz"
},
"price": 599.99,
"currency": "USD"
}
],
"store_id": "123e4567-e89b-12d3-a456-426614174000",
"category": "Electronics",
"active": true
}
}
}
},
"404": {
"description": "Store not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"code": 404,
"message": "Store not found"
}
}
}
}
}
}
},
"/products": {
"post": {
"summary": "Create a new product from template",
"operationId": "createProduct",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"template_id": {
"type": "string",
"format": "uuid"
},
"store_id": {
"type": "string",
"format": "uuid"
},
"stock_quantity": {
"type": "integer",
"minimum": 0
}
},
"required": [
"template_id",
"store_id",
"stock_quantity"
]
},
"examples": {
"newProduct": {
"summary": "Create a new Smart TV product",
"value": {
"template_id": "123e4567-e89b-12d3-a456-426614174002",
"store_id": "123e4567-e89b-12d3-a456-426614174000",
"stock_quantity": 50
}
}
}
}
}
},
"responses": {
"201": {
"description": "Product created successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Product"
},
"example": {
"id": "123e4567-e89b-12d3-a456-426614174003",
"template_id": "123e4567-e89b-12d3-a456-426614174002",
"name": "Smart TV 55-inch",
"description": "55-inch Smart TV with 4K Display",
"price": 899.99,
"currency": "USD",
"store_id": "123e4567-e89b-12d3-a456-426614174000",
"stock_quantity": 50,
"available": true
}
}
}
},
"404": {
"description": "Template or store not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"code": 404,
"message": "Product template not found"
}
}
}
}
}
}
},
"/orders": {
"post": {
"summary": "Create a new order",
"operationId": "createOrder",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"customer_id": {
"type": "string",
"format": "uuid"
},
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/OrderItem"
}
}
},
"required": [
"customer_id",
"items"
]
},
"examples": {
"newOrder": {
"summary": "Create an order for two Smart TVs",
"value": {
"customer_id": "123e4567-e89b-12d3-a456-426614174005",
"items": [
{
"product_id": "123e4567-e89b-12d3-a456-426614174003",
"quantity": 2,
"price": 899.99,
"currency": "USD"
}
]
}
}
}
}
}
},
"responses": {
"201": {
"description": "Order created successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Order"
},
"example": {
"id": "123e4567-e89b-12d3-a456-426614174004",
"customer_id": "123e4567-e89b-12d3-a456-426614174005",
"items": [
{
"product_id": "123e4567-e89b-12d3-a456-426614174003",
"quantity": 2,
"price": 899.99,
"currency": "USD"
}
],
"total_amount": 1799.98,
"currency": "USD",
"status": "pending",
"created_at": "2024-02-10T10:30:00Z",
"updated_at": "2024-02-10T10:30:00Z"
}
}
}
},
"400": {
"description": "Invalid input or insufficient stock",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"code": 400,
"message": "Insufficient stock for product"
}
}
}
}
}
}
},
"/orders/{orderId}/status": {
"put": {
"summary": "Update order status",
"operationId": "updateOrderStatus",
"parameters": [
{
"name": "orderId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
},
"example": "123e4567-e89b-12d3-a456-426614174004"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"pending",
"confirmed",
"shipped",
"delivered"
]
}
},
"required": [
"status"
]
},
"examples": {
"updateStatus": {
"summary": "Update order to shipped status",
"value": {
"status": "shipped"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Order status updated successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Order"
},
"example": {
"id": "123e4567-e89b-12d3-a456-426614174004",
"customer_id": "123e4567-e89b-12d3-a456-426614174005",
"items": [
{
"product_id": "123e4567-e89b-12d3-a456-426614174003",
"quantity": 2,
"price": 899.99,
"currency": "USD"
}
],
"total_amount": 1799.98,
"currency": "USD",
"status": "shipped",
"created_at": "2024-02-10T10:30:00Z",
"updated_at": "2024-02-10T10:35:00Z"
}
}
}
},
"404": {
"description": "Order not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"code": 404,
"message": "Order not found"
}
}
}
}
}
}
},
"/stores/{storeId}/products": {
"get": {
"summary": "Get all products for a store",
"operationId": "getStoreProducts",
"parameters": [
{
"name": "storeId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
},
"example": "123e4567-e89b-12d3-a456-426614174000"
}
],
"responses": {
"200": {
"description": "List of store's products",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Product"
}
},
"example": [
{
"id": "123e4567-e89b-12d3-a456-426614174003",
"template_id": "123e4567-e89b-12d3-a456-426614174002",
"name": "Smart TV 55-inch",
"description": "55-inch Smart TV with 4K Display",
"price": 899.99,
"currency": "USD",
"store_id": "123e4567-e89b-12d3-a456-426614174000",
"stock_quantity": 48,
"available": true
}
]
}
}
},
"404": {
"description": "Store not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"code": 404,
"message": "Store not found"
}
}
}
}
}
}
},
"/stores/{storeId}/orders": {
"get": {
"summary": "Get all orders for a store's products",
"operationId": "getStoreOrders",
"parameters": [
{
"name": "storeId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
},
"example": "123e4567-e89b-12d3-a456-426614174000"
}
],
"responses": {
"200": {
"description": "List of orders containing store's products",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Order"
}
},
"example": [
{
"id": "123e4567-e89b-12d3-a456-426614174004",
"customer_id": "123e4567-e89b-12d3-a456-426614174005",
"items": [
{
"product_id": "123e4567-e89b-12d3-a456-426614174003",
"quantity": 2,
"price": 899.99,
"currency": "USD"
}
],
"total_amount": 1799.98,
"currency": "USD",
"status": "shipped",
"created_at": "2024-02-10T10:30:00Z",
"updated_at": "2024-02-10T10:35:00Z"
}
]
}
}
},
"404": {
"description": "Store not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"code": 404,
"message": "Store not found"
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,81 @@
module geomind_poc
pub struct Merchant {
pub:
id string
name string
description string
contact string
active bool
}
pub struct ProductComponentTemplate {
pub:
id string
name string
description string
// technical specifications
specs map[string]string
// price per unit
price f64
// currency code (e.g., 'USD', 'EUR')
currency string
}
pub struct ProductTemplate {
pub:
id string
name string
description string
// components that make up this product template
components []ProductComponentTemplate
// merchant who created this template
merchant_id string
// category of the product (e.g., 'electronics', 'clothing')
category string
// whether this template is available for use
active bool
}
pub struct Product {
pub:
id string
template_id string
// specific instance details that may differ from template
name string
description string
// actual price of this product instance
price f64
currency string
// merchant selling this product
merchant_id string
// current stock level
stock_quantity int
// whether this product is available for purchase
available bool
}
pub struct OrderItem {
pub:
product_id string
quantity int
price f64
currency string
}
pub struct Order {
pub:
id string
// customer identifier
customer_id string
// items in the order
items []OrderItem
// total order amount
total_amount f64
currency string
// order status (e.g., 'pending', 'confirmed', 'shipped', 'delivered')
status string
// timestamps
created_at string
updated_at string
}

View File

@@ -0,0 +1,148 @@
module geomind_poc
import freeflowuniverse.crystallib.core.playbook { PlayBook }
// play_commerce processes heroscript actions for the commerce system
pub fn play_commerce(mut plbook playbook.PlayBook) ! {
commerce_actions := plbook.find(filter: 'commerce.')!
mut c := Commerce{}
for action in commerce_actions {
match action.name {
'merchant' {
mut p := action.params
merchant := c.create_merchant(
name: p.get('name')!,
description: p.get_default('description', '')!,
contact: p.get('contact')!
)!
println('Created merchant: ${merchant.name}')
}
'component' {
mut p := action.params
component := c.create_product_component_template(
name: p.get('name')!,
description: p.get_default('description', '')!,
specs: p.get_map(),
price: p.get_float('price')!,
currency: p.get('currency')!
)!
println('Created component: ${component.name}')
}
'template' {
mut p := action.params
// Get component IDs as a list
component_ids := p.get_list('components')!
// Convert component IDs to actual components
mut components := []ProductComponentTemplate{}
for id in component_ids {
// In a real implementation, you would fetch the component from storage
// For this example, we create a dummy component
component := ProductComponentTemplate{
id: id
name: 'Component'
description: ''
specs: map[string]string{}
price: 0
currency: 'USD'
}
components << component
}
template := c.create_product_template(
name: p.get('name')!,
description: p.get_default('description', '')!,
components: components,
merchant_id: p.get('merchant_id')!,
category: p.get_default('category', 'General')!
)!
println('Created template: ${template.name}')
}
'product' {
mut p := action.params
product := c.create_product(
template_id: p.get('template_id')!,
merchant_id: p.get('merchant_id')!,
stock_quantity: p.get_int('stock_quantity')!
)!
println('Created product: ${product.name} with stock: ${product.stock_quantity}')
}
'order' {
mut p := action.params
// Get order items as a list of maps
items_data := p.get_list('items')!
mut items := []OrderItem{}
for item_data in items_data {
// Parse item data (format: "product_id:quantity:price:currency")
parts := item_data.split(':')
if parts.len != 4 {
return error('Invalid order item format: ${item_data}')
}
item := OrderItem{
product_id: parts[0]
quantity: parts[1].int()
price: parts[2].f64()
currency: parts[3]
}
items << item
}
order := c.create_order(
customer_id: p.get('customer_id')!,
items: items
)!
println('Created order: ${order.id} with ${order.items.len} items')
}
'update_order' {
mut p := action.params
order := c.update_order_status(
order_id: p.get('order_id')!,
new_status: p.get('status')!
)!
println('Updated order ${order.id} status to: ${order.status}')
}
else {
return error('Unknown commerce action: ${action.name}')
}
}
}
}
// Example heroscript usage:
/*
!!commerce.merchant
name: "Tech Gadgets Store"
description: "Premium electronics and gadgets retailer"
contact: "contact@techgadgets.com"
!!commerce.component
name: "4K Display Panel"
description: "55-inch 4K UHD Display Panel"
specs:
resolution: "3840x2160"
refreshRate: "120Hz"
panel_type: "OLED"
price: 599.99
currency: "USD"
!!commerce.template
name: "Smart TV 55-inch"
description: "55-inch Smart TV with 4K Display"
components: "123e4567-e89b-12d3-a456-426614174001"
merchant_id: "123e4567-e89b-12d3-a456-426614174000"
category: "Electronics"
!!commerce.product
template_id: "123e4567-e89b-12d3-a456-426614174002"
merchant_id: "123e4567-e89b-12d3-a456-426614174000"
stock_quantity: 50
!!commerce.order
customer_id: "123e4567-e89b-12d3-a456-426614174005"
items:
- "123e4567-e89b-12d3-a456-426614174003:2:899.99:USD"
!!commerce.update_order
order_id: "123e4567-e89b-12d3-a456-426614174004"
status: "shipped"
*/

View File

@@ -0,0 +1,286 @@
{
"openapi": "3.0.1",
"info": {
"title": "Profiler",
"description": "API for managing user profiles with name, public key, and KYC verification",
"version": "1.0.0"
},
"servers": [
{
"url": "http://localhost:8080",
"description": "Local development server"
}
],
"components": {
"schemas": {
"Profile": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"example": "123e4567-e89b-12d3-a456-426614174000"
},
"name": {
"type": "string",
"example": "Alice Doe"
},
"public_key": {
"type": "string",
"example": "028a8f8b59f7283a47f9f6d4bc8176e847ad2b6c6d8bdfd041e5e7f3b4ac28c9fc"
},
"kyc_verified": {
"type": "boolean",
"example": false
}
},
"required": ["id", "name", "public_key", "kyc_verified"]
},
"Error": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"example": 400
},
"message": {
"type": "string",
"example": "Invalid request"
}
},
"required": ["code", "message"]
}
}
},
"paths": {
"/profiles": {
"post": {
"summary": "Create a new profile",
"operationId": "createProfile",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "Bob Smith"
},
"public_key": {
"type": "string",
"example": "03a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
}
},
"required": ["name", "public_key"]
},
"examples": {
"newProfile": {
"summary": "Example of creating a new profile",
"value": {
"name": "Bob Smith",
"public_key": "03a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
}
}
}
}
}
},
"responses": {
"201": {
"description": "Profile created successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Profile"
},
"examples": {
"successResponse": {
"summary": "Example of successful profile creation",
"value": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Bob Smith",
"public_key": "03a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
"kyc_verified": false
}
}
}
}
}
},
"400": {
"description": "Invalid input",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"examples": {
"invalidInput": {
"summary": "Example of invalid input error",
"value": {
"code": 400,
"message": "Invalid public key format"
}
}
}
}
}
}
}
}
},
"/profiles/{profileId}": {
"get": {
"summary": "Get profile details",
"operationId": "getProfile",
"parameters": [
{
"name": "profileId",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "uint32"
},
"example": "42"
}
],
"responses": {
"200": {
"description": "Profile retrieved successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Profile"
},
"examples": {
"existingProfile": {
"summary": "Example of retrieved profile",
"value": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Bob Smith",
"public_key": "03a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
"kyc_verified": true
}
}
}
}
}
},
"404": {
"description": "Profile not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"examples": {
"notFound": {
"summary": "Example of profile not found error",
"value": {
"code": 404,
"message": "Profile with ID '550e8400-e29b-41d4-a716-446655440000' not found"
}
}
}
}
}
}
}
}
},
"/profiles/{profileId}/kyc": {
"put": {
"summary": "Update KYC verification status",
"operationId": "updateKYCStatus",
"parameters": [
{
"name": "profileId",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "uint32"
},
"example": "42"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"kyc_verified": {
"type": "boolean",
"example": true
}
},
"required": ["kyc_verified"]
},
"examples": {
"verifyKYC": {
"summary": "Example of verifying KYC",
"value": {
"kyc_verified": true
}
},
"unverifyKYC": {
"summary": "Example of unverifying KYC",
"value": {
"kyc_verified": false
}
}
}
}
}
},
"responses": {
"200": {
"description": "KYC status updated successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Profile"
},
"examples": {
"updatedProfile": {
"summary": "Example of profile with updated KYC status",
"value": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Bob Smith",
"public_key": "03a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
"kyc_verified": true
}
}
}
}
}
},
"404": {
"description": "Profile not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"examples": {
"notFound": {
"summary": "Example of profile not found error",
"value": {
"code": 404,
"message": "Profile with ID '550e8400-e29b-41d4-a716-446655440000' not found"
}
}
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,190 @@
module geomind_poc
import crypto.rand
// Commerce represents the main e-commerce server handling all operations
pub struct Commerce {
mut:
merchants map[string]Merchant
templates map[string]ProductTemplate
products map[string]Product
orders map[string]Order
}
// generate_id creates a unique identifier
fn generate_id() string {
return rand.uuid_v4()
}
// create_merchant adds a new merchant to the system
pub fn (mut c Commerce) create_merchant(name string, description string, contact string) !Merchant {
merchant_id := generate_id()
merchant := Merchant{
id: merchant_id
name: name
description: description
contact: contact
active: true
}
c.merchants[merchant_id] = merchant
return merchant
}
// create_product_component_template creates a new component template
pub fn (mut c Commerce) create_product_component_template(name string, description string, specs map[string]string, price f64, currency string) !ProductComponentTemplate {
component := ProductComponentTemplate{
id: generate_id()
name: name
description: description
specs: specs
price: price
currency: currency
}
return component
}
// create_product_template creates a new product template
pub fn (mut c Commerce) create_product_template(name string, description string, components []ProductComponentTemplate, merchant_id string, category string) !ProductTemplate {
if merchant_id !in c.merchants {
return error('Merchant not found')
}
template := ProductTemplate{
id: generate_id()
name: name
description: description
components: components
merchant_id: merchant_id
category: category
active: true
}
c.templates[template.id] = template
return template
}
// create_product creates a new product instance from a template
pub fn (mut c Commerce) create_product(template_id string, merchant_id string, stock_quantity int) !Product {
if template_id !in c.templates {
return error('Template not found')
}
if merchant_id !in c.merchants {
return error('Merchant not found')
}
template := c.templates[template_id]
mut total_price := 0.0
for component in template.components {
total_price += component.price
}
product := Product{
id: generate_id()
template_id: template_id
name: template.name
description: template.description
price: total_price
currency: template.components[0].currency // assuming all components use same currency
merchant_id: merchant_id
stock_quantity: stock_quantity
available: true
}
c.products[product.id] = product
return product
}
// create_order creates a new order
pub fn (mut c Commerce) create_order(customer_id string, items []OrderItem) !Order {
mut total_amount := 0.0
mut currency := ''
for item in items {
if item.product_id !in c.products {
return error('Product not found: ${item.product_id}')
}
product := c.products[item.product_id]
if !product.available || product.stock_quantity < item.quantity {
return error('Product ${product.name} is not available in requested quantity')
}
total_amount += item.price * item.quantity
if currency == '' {
currency = item.currency
} else if currency != item.currency {
return error('Mixed currencies are not supported')
}
}
order := Order{
id: generate_id()
customer_id: customer_id
items: items
total_amount: total_amount
currency: currency
status: 'pending'
created_at: time.now().str()
updated_at: time.now().str()
}
c.orders[order.id] = order
// Update stock quantities
for item in items {
mut product := c.products[item.product_id]
product.stock_quantity -= item.quantity
if product.stock_quantity == 0 {
product.available = false
}
c.products[item.product_id] = product
}
return order
}
// update_order_status updates the status of an order
pub fn (mut c Commerce) update_order_status(order_id string, new_status string) !Order {
if order_id !in c.orders {
return error('Order not found')
}
mut order := c.orders[order_id]
order.status = new_status
order.updated_at = time.now().str()
c.orders[order_id] = order
return order
}
// get_merchant_products returns all products for a given merchant
pub fn (c Commerce) get_merchant_products(merchant_id string) ![]Product {
if merchant_id !in c.merchants {
return error('Merchant not found')
}
mut products := []Product{}
for product in c.products.values() {
if product.merchant_id == merchant_id {
products << product
}
}
return products
}
// get_merchant_orders returns all orders for products sold by a merchant
pub fn (c Commerce) get_merchant_orders(merchant_id string) ![]Order {
if merchant_id !in c.merchants {
return error('Merchant not found')
}
mut orders := []Order{}
for order in c.orders.values() {
mut includes_merchant := false
for item in order.items {
product := c.products[item.product_id]
if product.merchant_id == merchant_id {
includes_merchant = true
break
}
}
if includes_merchant {
orders << order
}
}
return orders
}

View File

@@ -0,0 +1,57 @@
- profile management
- my name
- my pub key
- kyc
- ...
- product has components
- admin items
- supported_currencies
- countries
- continents
- farming
- farms
- default farm exists, users don't have to chose
- name
- description
- owner (pubkey)
- nodes
- reward (nr of INCA per month and time e.g. 24 months)
- reward_promised
- reward_given
- location
- coordinates
- continent
- country
- description
- farmid
- capacity (disks, mem, ...)
- gridversion (eg. 3.16)
- nodestats
- ...
- uptime
- bandwidth
- referral system
- coupons for discounts (one product can have multiple coupons and discounts)
- data gets imported with heroscript for what we sell
- minimal wallet function (BTC, CHF, MGLD, TFT, INCA)
- transactions, so they can see what they spend money on
- transfer/exchange
- basic communication (messages in/out)
- to allow us to communicate with user
- news
- basic news feed with topics, which we can set
- vdc
- name
- description (optional)
- spendinglimit
- currency per month, week or day e.g. 0.1 BTC/month
- each spending limit has name
- admins, list of pubkeys who have access to this and can add capacity to it, or delete, ...
- deployment
- deploymentid
- vdcid
- heroscript
- status
- links (name, link, description, category)

View File

@@ -0,0 +1,47 @@
#!/usr/bin/env -S v
import freeflowuniverse.crystallib.core.playbook
import geomind_poc
fn main() {
test_script := "
!!commerce.merchant
name: 'Tech Gadgets Store'
description: 'Premium electronics and gadgets retailer'
contact: 'contact@techgadgets.com'
!!commerce.component
name: '4K Display Panel'
description: '55-inch 4K UHD Display Panel'
specs:
resolution: '3840x2160'
refreshRate: '120Hz'
panel_type: 'OLED'
price: 599.99
currency: 'USD'
!!commerce.template
name: 'Smart TV 55-inch'
description: '55-inch Smart TV with 4K Display'
components: '123e4567-e89b-12d3-a456-426614174001'
merchant_id: '123e4567-e89b-12d3-a456-426614174000'
category: 'Electronics'
!!commerce.product
template_id: '123e4567-e89b-12d3-a456-426614174002'
merchant_id: '123e4567-e89b-12d3-a456-426614174000'
stock_quantity: 50
!!commerce.order
customer_id: '123e4567-e89b-12d3-a456-426614174005'
items:
- '123e4567-e89b-12d3-a456-426614174003:2:899.99:USD'
!!commerce.update_order
order_id: '123e4567-e89b-12d3-a456-426614174004'
status: 'shipped'
"
mut plbook := playbook.new(text: test_script)!
geomind_poc.play_commerce(mut plbook)!
}

View File

@@ -0,0 +1,3 @@
methods.v
meeting_scheduler_actor
generate_actor_module

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.baobab.generator
import freeflowuniverse.herolib.baobab.specification
import freeflowuniverse.herolib.schemas.openapi
import os
const example_dir = os.dir(@FILE)
const openapi_spec_path = os.join_path(example_dir, 'openapi.json')
// the actor specification obtained from the OpenRPC Specification
openapi_spec := openapi.new(path: openapi_spec_path)!
actor_spec := specification.from_openapi(openapi_spec)!
println(actor_spec)
actor_module := generator.generate_actor_module(
actor_spec,
interfaces: [.openapi, .http]
)!
actor_module.write(example_dir,
format: false
overwrite: true
compile: false
)!
os.execvp('bash', ['${example_dir}/meeting_scheduler_actor/scripts/run.sh'])!

View File

@@ -0,0 +1,311 @@
{
"openapi": "3.0.0",
"info": {
"title": "Meeting Scheduler",
"version": "1.0.0",
"description": "An API for managing meetings, availability, and scheduling."
},
"servers": [
{
"url": "http://localhost:8080/openapi/v1",
"description": "Production server"
},
{
"url": "http://localhost:8081/openapi/v1",
"description": "Example server"
}
],
"paths": {
"/users": {
"get": {
"summary": "List all users",
"responses": {
"200": {
"description": "A list of users",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/User"
}
},
"example": [
{
"id": "1",
"name": "Alice",
"email": "alice@example.com"
},
{
"id": "2",
"name": "Bob",
"email": "bob@example.com"
}
]
}
}
}
}
}
},
"/users/{userId}": {
"get": {
"operationId": "get_user",
"summary": "Get user by ID",
"parameters": [
{
"name": "userId",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "uint32"
},
"description": "The ID of the user",
"example": 1
}
],
"responses": {
"200": {
"description": "User details",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
},
"example": {
"id": "1",
"name": "Alice",
"email": "alice@example.com"
}
}
}
},
"404": {
"description": "User not found"
}
}
}
},
"/events": {
"post": {
"summary": "Create an event",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Event"
},
"example": {
"title": "Team Meeting",
"description": "Weekly sync",
"startTime": "2023-10-10T10:00:00Z",
"endTime": "2023-10-10T11:00:00Z",
"userId": "1"
}
}
}
},
"responses": {
"201": {
"description": "Event created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Event"
},
"example": {
"id": "101",
"title": "Team Meeting",
"description": "Weekly sync",
"startTime": "2023-10-10T10:00:00Z",
"endTime": "2023-10-10T11:00:00Z",
"userId": "1"
}
}
}
}
}
}
},
"/availability": {
"get": {
"summary": "Get availability for a user",
"parameters": [
{
"name": "userId",
"in": "query",
"required": true,
"schema": {
"type": "string"
},
"description": "The ID of the user",
"example": "1"
},
{
"name": "date",
"in": "query",
"required": false,
"schema": {
"type": "string",
"format": "date"
},
"description": "The date to check availability (YYYY-MM-DD)",
"example": "2023-10-10"
}
],
"responses": {
"200": {
"description": "Availability details",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/TimeSlot"
}
},
"example": [
{
"startTime": "10:00:00",
"endTime": "11:00:00",
"available": true
},
{
"startTime": "11:00:00",
"endTime": "12:00:00",
"available": false
}
]
}
}
}
}
}
},
"/bookings": {
"post": {
"summary": "Book a meeting",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Booking"
},
"example": {
"userId": "1",
"eventId": "101",
"timeSlot": {
"startTime": "10:00:00",
"endTime": "11:00:00",
"available": true
}
}
}
}
},
"responses": {
"201": {
"description": "Booking created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Booking"
},
"example": {
"id": "5001",
"userId": "1",
"eventId": "101",
"timeSlot": {
"startTime": "10:00:00",
"endTime": "11:00:00",
"available": true
}
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"User": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"email": {
"type": "string",
"format": "email"
}
}
},
"Event": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"startTime": {
"type": "string",
"format": "date-time"
},
"endTime": {
"type": "string",
"format": "date-time"
},
"userId": {
"type": "string"
}
}
},
"TimeSlot": {
"type": "object",
"properties": {
"startTime": {
"type": "string",
"format": "time"
},
"endTime": {
"type": "string",
"format": "time"
},
"available": {
"type": "boolean"
}
}
},
"Booking": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"userId": {
"type": "string"
},
"eventId": {
"type": "string"
},
"timeSlot": {
"$ref": "#/components/schemas/TimeSlot"
}
}
}
}
}
}

View File

@@ -0,0 +1,3 @@
# Actor Specification Examples
These examples show how `OpenRPC` and `OpenAPI` specifications can be translated back and forth into an `ActorSpecification`. This is an important step of actor generation as actor code is generated from actor specification.

View File

@@ -0,0 +1,346 @@
{
"openapi": "3.0.3",
"info": {
"title": "Pet Store API",
"description": "A sample API for a pet store",
"version": "1.0.0"
},
"servers": [
{
"url": "https://api.petstore.example.com/v1",
"description": "Production server"
},
{
"url": "https://staging.petstore.example.com/v1",
"description": "Staging server"
}
],
"paths": {
"/pets": {
"get": {
"summary": "List all pets",
"operationId": "listPets",
"parameters": [
{
"name": "limit",
"in": "query",
"description": "Maximum number of pets to return",
"required": false,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "A paginated list of pets",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Pets"
}
}
}
},
"400": {
"description": "Invalid request"
}
}
},
"post": {
"summary": "Create a new pet",
"operationId": "createPet",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NewPet"
}
}
}
},
"responses": {
"201": {
"description": "Pet created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
}
}
},
"400": {
"description": "Invalid input"
}
}
}
},
"/pets/{petId}": {
"get": {
"summary": "Get a pet by ID",
"operationId": "getPet",
"parameters": [
{
"name": "petId",
"in": "path",
"description": "ID of the pet to retrieve",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
}
],
"responses": {
"200": {
"description": "A pet",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
}
}
},
"404": {
"description": "Pet not found"
}
}
},
"delete": {
"summary": "Delete a pet by ID",
"operationId": "deletePet",
"parameters": [
{
"name": "petId",
"in": "path",
"description": "ID of the pet to delete",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
}
],
"responses": {
"204": {
"description": "Pet deleted"
},
"404": {
"description": "Pet not found"
}
}
}
},
"/orders": {
"get": {
"summary": "List all orders",
"operationId": "listOrders",
"responses": {
"200": {
"description": "A list of orders",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Order"
}
}
}
}
}
}
}
},
"/orders/{orderId}": {
"get": {
"summary": "Get an order by ID",
"operationId": "getOrder",
"parameters": [
{
"name": "orderId",
"in": "path",
"description": "ID of the order to retrieve",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
}
],
"responses": {
"200": {
"description": "An order",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Order"
}
}
}
},
"404": {
"description": "Order not found"
}
}
},
"delete": {
"summary": "Delete an order by ID",
"operationId": "deleteOrder",
"parameters": [
{
"name": "orderId",
"in": "path",
"description": "ID of the order to delete",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
}
],
"responses": {
"204": {
"description": "Order deleted"
},
"404": {
"description": "Order not found"
}
}
}
},
"/users": {
"post": {
"summary": "Create a user",
"operationId": "createUser",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NewUser"
}
}
}
},
"responses": {
"201": {
"description": "User created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"Pet": {
"type": "object",
"required": ["id", "name"],
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
},
"tag": {
"type": "string"
}
}
},
"NewPet": {
"type": "object",
"required": ["name"],
"properties": {
"name": {
"type": "string"
},
"tag": {
"type": "string"
}
}
},
"Pets": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Pet"
}
},
"Order": {
"type": "object",
"required": ["id", "petId", "quantity", "shipDate"],
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"petId": {
"type": "integer",
"format": "int64"
},
"quantity": {
"type": "integer",
"format": "int32"
},
"shipDate": {
"type": "string",
"format": "date-time"
},
"status": {
"type": "string",
"enum": ["placed", "approved", "delivered"]
},
"complete": {
"type": "boolean"
}
}
},
"User": {
"type": "object",
"required": ["id", "username"],
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"username": {
"type": "string"
},
"email": {
"type": "string"
},
"phone": {
"type": "string"
}
}
},
"NewUser": {
"type": "object",
"required": ["username"],
"properties": {
"username": {
"type": "string"
},
"email": {
"type": "string"
},
"phone": {
"type": "string"
}
}
}
}
}
}

View File

@@ -0,0 +1,13 @@
#!/usr/bin/env -S v -w -n -enable-globals run
import freeflowuniverse.herolib.baobab.specification
import freeflowuniverse.herolib.schemas.openapi
import os
const example_dir = os.dir(@FILE)
const openapi_spec_path = os.join_path(example_dir, 'openapi.json')
// the actor specification obtained from the OpenRPC Specification
openapi_spec := openapi.new(path: openapi_spec_path)!
actor_specification := specification.from_openapi(openapi_spec)!
println(actor_specification)

View File

@@ -0,0 +1,132 @@
{
"openrpc": "1.0.0",
"info": {
"title": "PetStore",
"version": "1.0.0"
},
"methods": [
{
"name": "GetPets",
"description": "finds pets in the system that the user has access to by tags and within a limit",
"params": [
{
"name": "tags",
"description": "tags to filter by",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
},
{
"name": "limit",
"description": "maximum number of results to return",
"schema": {
"type": "integer"
}
}
],
"result": {
"name": "pet_list",
"description": "all pets from the system, that mathes the tags",
"schema": {
"$ref": "#\/components\/schemas\/Pet"
}
}
},
{
"name": "CreatePet",
"description": "creates a new pet in the store. Duplicates are allowed.",
"params": [
{
"name": "new_pet",
"description": "Pet to add to the store.",
"schema": {
"$ref": "#\/components\/schemas\/NewPet"
}
}
],
"result": {
"name": "pet",
"description": "the newly created pet",
"schema": {
"$ref": "#\/components\/schemas\/Pet"
}
}
},
{
"name": "GetPetById",
"description": "gets a pet based on a single ID, if the user has access to the pet",
"params": [
{
"name": "id",
"description": "ID of pet to fetch",
"schema": {
"type": "integer"
}
}
],
"result": {
"name": "pet",
"description": "pet response",
"schema": {
"$ref": "#\/components\/schemas\/Pet"
}
}
},
{
"name": "DeletePetById",
"description": "deletes a single pet based on the ID supplied",
"params": [
{
"name": "id",
"description": "ID of pet to delete",
"schema": {
"type": "integer"
}
}
],
"result": {
"name": "pet",
"description": "pet deleted",
"schema": {
"type": "null"
}
}
}
],
"components": {
"schemas": {
"NewPet": {
"title": "NewPet",
"properties": {
"name": {
"type": "string"
},
"tag": {
"type": "string"
}
}
},
"Pet": {
"title": "Pet",
"description": "a pet struct that represents a pet",
"properties": {
"name": {
"description": "name of the pet",
"type": "string"
},
"tag": {
"description": "a tag of the pet, helps finding pet",
"type": "string"
},
"id": {
"description": "unique indentifier",
"type": "integer"
}
}
}
}
}
}

View File

@@ -0,0 +1,13 @@
#!/usr/bin/env -S v -w -n -enable-globals run
import freeflowuniverse.herolib.baobab.specification
import freeflowuniverse.herolib.schemas.openrpc
import os
const example_dir = os.dir(@FILE)
const openrpc_spec_path = os.join_path(example_dir, 'openrpc.json')
// the actor specification obtained from the OpenRPC Specification
openrpc_spec := openrpc.new(path: openrpc_spec_path)!
actor_specification := specification.from_openrpc(openrpc_spec)!
println(actor_specification)

View File

@@ -0,0 +1,107 @@
#!/usr/bin/env -S v -w -n -enable-globals run
import json
import freeflowuniverse.herolib.baobab.specification
import freeflowuniverse.herolib.schemas.jsonschema
import freeflowuniverse.herolib.schemas.openrpc
import os
const actor_specification = specification.ActorSpecification{
name: 'PetStore'
interfaces: [.openrpc]
methods: [
specification.ActorMethod{
name: 'GetPets'
description: 'finds pets in the system that the user has access to by tags and within a limit'
parameters: [
openrpc.ContentDescriptor{
name: 'tags'
description: 'tags to filter by'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'array'
items: jsonschema.Items(jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
}))
})
},
openrpc.ContentDescriptor{
name: 'limit'
description: 'maximum number of results to return'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'integer'
})
}
]
result: openrpc.ContentDescriptor{
name: 'pet_list'
description: 'all pets from the system, that matches the tags'
schema: jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/Pet'
})
}
},
specification.ActorMethod{
name: 'CreatePet'
description: 'creates a new pet in the store. Duplicates are allowed.'
parameters: [
openrpc.ContentDescriptor{
name: 'new_pet'
description: 'Pet to add to the store.'
schema: jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/NewPet'
})
}
]
result: openrpc.ContentDescriptor{
name: 'pet'
description: 'the newly created pet'
schema: jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/Pet'
})
}
},
specification.ActorMethod{
name: 'GetPetById'
description: 'gets a pet based on a single ID, if the user has access to the pet'
parameters: [
openrpc.ContentDescriptor{
name: 'id'
description: 'ID of pet to fetch'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'integer'
})
}
]
result: openrpc.ContentDescriptor{
name: 'pet'
description: 'pet response'
schema: jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/Pet'
})
}
},
specification.ActorMethod{
name: 'DeletePetById'
description: 'deletes a single pet based on the ID supplied'
parameters: [
openrpc.ContentDescriptor{
name: 'id'
description: 'ID of pet to delete'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'integer'
})
}
]
result: openrpc.ContentDescriptor{
name: 'pet'
description: 'pet deleted'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'null'
})
}
}
]
}
openapi_specification := actor_specification.to_openapi()
println(json.encode_pretty(openapi_specification))

View File

@@ -0,0 +1,109 @@
#!/usr/bin/env -S v -w -n -enable-globals run
import json
import freeflowuniverse.herolib.baobab.specification
import freeflowuniverse.herolib.core.code
import freeflowuniverse.herolib.schemas.jsonschema
import freeflowuniverse.herolib.schemas.openrpc
import os
const actor_specification = specification.ActorSpecification{
name: 'PetStore'
structure: code.Struct{}
interfaces: [.openrpc]
methods: [
specification.ActorMethod{
name: 'GetPets'
description: 'finds pets in the system that the user has access to by tags and within a limit'
parameters: [
openrpc.ContentDescriptor{
name: 'tags'
description: 'tags to filter by'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'array'
items: jsonschema.Items(jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
}))
})
},
openrpc.ContentDescriptor{
name: 'limit'
description: 'maximum number of results to return'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'integer'
})
}
]
result: openrpc.ContentDescriptor{
name: 'pet_list'
description: 'all pets from the system, that matches the tags'
schema: jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/Pet'
})
}
},
specification.ActorMethod{
name: 'CreatePet'
description: 'creates a new pet in the store. Duplicates are allowed.'
parameters: [
openrpc.ContentDescriptor{
name: 'new_pet'
description: 'Pet to add to the store.'
schema: jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/NewPet'
})
}
]
result: openrpc.ContentDescriptor{
name: 'pet'
description: 'the newly created pet'
schema: jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/Pet'
})
}
},
specification.ActorMethod{
name: 'GetPetById'
description: 'gets a pet based on a single ID, if the user has access to the pet'
parameters: [
openrpc.ContentDescriptor{
name: 'id'
description: 'ID of pet to fetch'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'integer'
})
}
]
result: openrpc.ContentDescriptor{
name: 'pet'
description: 'pet response'
schema: jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/Pet'
})
}
},
specification.ActorMethod{
name: 'DeletePetById'
description: 'deletes a single pet based on the ID supplied'
parameters: [
openrpc.ContentDescriptor{
name: 'id'
description: 'ID of pet to delete'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'integer'
})
}
]
result: openrpc.ContentDescriptor{
name: 'pet'
description: 'pet deleted'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'null'
})
}
}
]
}
openrpc_specification := actor_specification.to_openrpc()
println(json.encode_pretty(openrpc_specification))

View File

@@ -7,7 +7,7 @@ import os
const testpath3 = os.dir(@FILE) + '/../..'
// if we return True then it means the dir or file is processed
fn filter_1(mut path pathlib.Path, mut params paramsparser.Params) !bool {
fn filter_1(mut path pathlib.Path) !bool {
if path.is_dir() {
if path.path.ends_with('.dSYM') {
return false

View File

@@ -1,7 +1,7 @@
module example_actor
import os
import freeflowuniverse.herolib.hero.baobab.actor { IActor, RunParams }
import freeflowuniverse.herolib.hero.baobab.stage {IActor, RunParams}
import freeflowuniverse.herolib.web.openapi
import time
@@ -10,11 +10,13 @@ const openapi_spec_json = os.read_file(openapi_spec_path) or { panic(err) }
const openapi_specification = openapi.json_decode(openapi_spec_json)!
struct ExampleActor {
actor.Actor
stage.Actor
}
fn new() !ExampleActor {
return ExampleActor{actor.new('example')}
return ExampleActor{
stage.new_actor('example')
}
}
pub fn run() ! {

View File

@@ -70,87 +70,74 @@ fn (mut actor Actor) listen() ! {
// Handle method invocations
fn (mut actor Actor) handle_method(cmd string, data string) !string {
println('debugzo received rpc ${cmd}:${data}')
param_anys := json2.raw_decode(data)!.arr()
match cmd {
'listPets' {
pets := if param_anys.len == 0 {
actor.data_store.list_pets()
} else {
params := json.decode(ListPetParams, param_anys[0].str())!
actor.data_store.list_pets(params)
}
return json.encode(pets)
}
'createPet' {
response := if param_anys.len == 0 {
return error('at least data expected')
} else if param_anys.len == 1 {
payload := json.decode(NewPet, param_anys[0].str())!
actor.data_store.create_pet(payload)
} else {
return error('expected 1 param, found too many')
}
// data := json.decode(NewPet, data) or { return error('Invalid pet data: $err') }
// created_pet := actor.data_store.create_pet(pet)
return json.encode(response)
}
'getPet' {
response := if param_anys.len == 0 {
return error('at least data expected')
} else if param_anys.len == 1 {
payload := param_anys[0].int()
actor.data_store.get_pet(payload)!
} else {
return error('expected 1 param, found too many')
}
return json.encode(response)
}
'deletePet' {
params := json.decode(map[string]int, data) or {
return error('Invalid params: ${err}')
}
actor.data_store.delete_pet(params['petId']) or {
return error('Pet not found: ${err}')
}
return json.encode({
'message': 'Pet deleted'
})
}
'listOrders' {
orders := actor.data_store.list_orders()
return json.encode(orders)
}
'getOrder' {
params := json.decode(map[string]int, data) or {
return error('Invalid params: ${err}')
}
order := actor.data_store.get_order(params['orderId']) or {
return error('Order not found: ${err}')
}
return json.encode(order)
}
'deleteOrder' {
params := json.decode(map[string]int, data) or {
return error('Invalid params: ${err}')
}
actor.data_store.delete_order(params['orderId']) or {
return error('Order not found: ${err}')
}
return json.encode({
'message': 'Order deleted'
})
}
'createUser' {
user := json.decode(NewUser, data) or { return error('Invalid user data: ${err}') }
created_user := actor.data_store.create_user(user)
return json.encode(created_user)
}
else {
return error('Unknown method: ${cmd}')
}
}
param_anys := json2.raw_decode(data)!.arr()
match cmd {
'listPets' {
pets := if param_anys.len == 0 {
actor.data_store.list_pets()
} else {
params := json.decode(ListPetParams, param_anys[0].str())!
actor.data_store.list_pets(params)
}
return json.encode(pets)
}
'createPet' {
response := if param_anys.len == 0 {
return error('at least data expected')
} else if param_anys.len == 1 {
payload := json.decode(NewPet, param_anys[0].str())!
actor.data_store.create_pet(payload)
} else {
return error('expected 1 param, found too many')
}
// data := json.decode(NewPet, data) or { return error('Invalid pet data: $err') }
// created_pet := actor.data_store.create_pet(pet)
return json.encode(response)
}
'getPet' {
response := if param_anys.len == 0 {
return error('at least data expected')
} else if param_anys.len == 1 {
payload := param_anys[0].int()
actor.data_store.get_pet(payload)!
} else {
return error('expected 1 param, found too many')
}
return json.encode(response)
}
'deletePet' {
params := json.decode(map[string]int, data) or { return error('Invalid params: $err') }
actor.data_store.delete_pet(params['petId']) or { return error('Pet not found: $err') }
return json.encode({'message': 'Pet deleted'})
}
'listOrders' {
orders := actor.data_store.list_orders()
return json.encode(orders)
}
'getOrder' {
params := json.decode(map[string]int, data) or { return error('Invalid params: $err') }
order := actor.data_store.get_order(params['orderId']) or {
return error('Order not found: $err')
}
return json.encode(order)
}
'deleteOrder' {
params := json.decode(map[string]int, data) or { return error('Invalid params: $err') }
actor.data_store.delete_order(params['orderId']) or {
return error('Order not found: $err')
}
return json.encode({'message': 'Order deleted'})
}
'createUser' {
user := json.decode(NewUser, data) or { return error('Invalid user data: $err') }
created_user := actor.data_store.create_user(user)
return json.encode(created_user)
}
else {
return error('Unknown method: $cmd')
}
}
}
@[params]

View File

@@ -0,0 +1 @@
client_typescript

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env -S v -n -w -enable-globals run
// Calendar Typescript Client Generation Example
// This example demonstrates how to generate a typescript client
// from a given OpenAPI Specification using the `openapi/codegen` module.
import os
import freeflowuniverse.herolib.schemas.openapi
import freeflowuniverse.herolib.schemas.openapi.codegen
const dir = os.dir(@FILE)
const specification = openapi.new(path: '${dir}/meeting_api.json') or {
panic('this should never happen ${err}')
}
// generate typescript client folder and write it in dir
codegen.ts_client_folder(specification)!.write(dir, overwrite: true)!

View File

@@ -0,0 +1,245 @@
{
"openapi": "3.0.0",
"info": {
"title": "Meeting Scheduler API",
"version": "1.0.0",
"description": "An API for managing meetings, availability, and scheduling."
},
"servers": [
{
"url": "https://api.meetingscheduler.com/v1",
"description": "Production server"
},
{
"url": "https://sandbox.api.meetingscheduler.com/v1",
"description": "Sandbox server"
}
],
"paths": {
"/users": {
"get": {
"summary": "List all users",
"responses": {
"200": {
"description": "A list of users",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/User"
}
}
}
}
}
}
}
},
"/users/{userId}": {
"get": {
"operationId": "get_user",
"summary": "Get user by ID",
"parameters": [
{
"name": "userId",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "The ID of the user"
}
],
"responses": {
"200": {
"description": "User details",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
},
"404": {
"description": "User not found"
}
}
}
},
"/events": {
"post": {
"summary": "Create an event",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Event"
}
}
}
},
"responses": {
"201": {
"description": "Event created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Event"
}
}
}
}
}
}
},
"/availability": {
"get": {
"summary": "Get availability for a user",
"parameters": [
{
"name": "userId",
"in": "query",
"required": true,
"schema": {
"type": "string"
},
"description": "The ID of the user"
},
{
"name": "date",
"in": "query",
"required": false,
"schema": {
"type": "string",
"format": "date"
},
"description": "The date to check availability (YYYY-MM-DD)"
}
],
"responses": {
"200": {
"description": "Availability details",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/TimeSlot"
}
}
}
}
}
}
}
},
"/bookings": {
"post": {
"summary": "Book a meeting",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Booking"
}
}
}
},
"responses": {
"201": {
"description": "Booking created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Booking"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"User": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"email": {
"type": "string",
"format": "email"
}
}
},
"Event": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"startTime": {
"type": "string",
"format": "date-time"
},
"endTime": {
"type": "string",
"format": "date-time"
},
"userId": {
"type": "string"
}
}
},
"TimeSlot": {
"type": "object",
"properties": {
"startTime": {
"type": "string",
"format": "time"
},
"endTime": {
"type": "string",
"format": "time"
},
"available": {
"type": "boolean"
}
}
},
"Booking": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"userId": {
"type": "string"
},
"eventId": {
"type": "string"
},
"timeSlot": {
"$ref": "#/components/schemas/TimeSlot"
}
}
}
}
}
}

View File

@@ -1,8 +1,8 @@
module actor
import json
import freeflowuniverse.herolib.core.redisclient
import freeflowuniverse.herolib.hero.baobab.action { ProcedureCall, ProcedureResponse }
import freeflowuniverse.herolib.clients.redisclient
import freeflowuniverse.herolib.baobab.action { ProcedureCall, ProcedureResponse }
// Processor struct for managing procedure calls
pub struct Client {
@@ -14,7 +14,7 @@ pub mut:
@[params]
pub struct Params {
pub:
timeout int // Timeout in seconds
timeout int = 60 // Timeout in seconds
}
pub struct ClientConfig {
@@ -62,8 +62,6 @@ pub fn (mut p Client) call_to_action(action Procedure, params Params) !Procedure
// }
}
println('resp data ${response_data}')
return ProcedureResponse{
result: response_data
}

View File

@@ -0,0 +1,118 @@
module generator
import freeflowuniverse.herolib.core.code {Folder, File}
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.schemas.jsonschema.codegen { schema_to_struct }
import freeflowuniverse.herolib.schemas.openrpc.codegen as openrpc_codegen { content_descriptor_to_parameter }
import freeflowuniverse.herolib.baobab.specification {ActorSpecification, ActorMethod, BaseObject}
import net.http
// pub enum BaseObjectMethodType {
// new
// get
// set
// delete
// list
// other
// }
// pub struct BaseObjectMethod {
// pub:
// typ BaseObjectMethodType
// object string // the name of the base object
// }
// pub fn ts_client_get_fn(object string, params TSClientFunctionParams) string {
// name_snake := texttools.snake_case(object)
// name_pascal := texttools.name_fix_pascal(object)
// root := get_endpoint_root(params.endpoint)
// return "async get${name_pascal}(id: string): Promise<${name_pascal}> {\n return this.restClient.get<${name_pascal}>(`/${root}/${name_snake}/\${id}`);\n }"
// }
// pub fn ts_client_set_fn(object string, params TSClientFunctionParams) string {
// name_snake := texttools.snake_case(object)
// name_pascal := texttools.name_fix_pascal(object)
// root := get_endpoint_root(params.endpoint)
// return "async set${name_pascal}(id: string, ${name_snake}: Partial<${name_pascal}>): Promise<${name_pascal}> {\n return this.restClient.put<${name_pascal}>(`/${root}/${name_snake}/\${id}`, ${name_snake});\n }"
// }
// pub fn ts_client_delete_fn(object string, params TSClientFunctionParams) string {
// name_snake := texttools.snake_case(object)
// name_pascal := texttools.name_fix_pascal(object)
// root := get_endpoint_root(params.endpoint)
// return "async delete${name_pascal}(id: string): Promise<void> {\n return this.restClient.delete<void>(`/${root}/${name_snake}/\${id}`);\n }"
// }
// pub fn ts_client_list_fn(object string, params TSClientFunctionParams) string {
// name_snake := texttools.snake_case(object)
// name_pascal := texttools.name_fix_pascal(object)
// root := get_endpoint_root(params.endpoint)
// return "async list${name_pascal}(): Promise<${name_pascal}[]> {\n return this.restClient.get<${name_pascal}[]>(`/${root}/${name_snake}`);\n }"
// }
fn get_endpoint_root(root string) string {
return if root == '' {
''
} else {
"/${root.trim('/')}"
}
}
// // generates a Base Object's `create` method
// pub fn ts_client_new_fn(object string, params TSClientFunctionParams) string {
// name_snake := texttools.snake_case(object)
// name_pascal := texttools.name_fix_pascal(object)
// root := get_endpoint_root(params.endpoint)
// return "async create${name_snake}(object: Omit<${name_pascal}, 'id'>): Promise<${name_pascal}> {
// return this.restClient.post<${name_pascal}>('${root}/${name_snake}', board);
// }"
// }
// pub fn ts_client_get_fn(object string, params TSClientFunctionParams) string {
// name_snake := texttools.snake_case(object)
// name_pascal := texttools.name_fix_pascal(object)
// root := get_endpoint_root(params.endpoint)
// return "async get${name_pascal}(id: string): Promise<${name_pascal}> {\n return this.restClient.get<${name_pascal}>(`/${root}/${name_snake}/\${id}`);\n }"
// }
// pub fn ts_client_set_fn(object string, params TSClientFunctionParams) string {
// name_snake := texttools.snake_case(object)
// name_pascal := texttools.name_fix_pascal(object)
// root := get_endpoint_root(params.endpoint)
// return "async set${name_pascal}(id: string, ${name_snake}: Partial<${name_pascal}>): Promise<${name_pascal}> {\n return this.restClient.put<${name_pascal}>(`/${root}/${name_snake}/\${id}`, ${name_snake});\n }"
// }
// pub fn ts_client_delete_fn(object string, params TSClientFunctionParams) string {
// name_snake := texttools.snake_case(object)
// name_pascal := texttools.name_fix_pascal(object)
// root := get_endpoint_root(params.endpoint)
// return "async delete${name_pascal}(id: string): Promise<void> {\n return this.restClient.delete<void>(`/${root}/${name_snake}/\${id}`);\n }"
// }
// pub fn ts_client_list_fn(object string, params TSClientFunctionParams) string {
// name_snake := texttools.snake_case(object)
// name_pascal := texttools.name_fix_pascal(object)
// root := get_endpoint_root(params.endpoint)
// return "async list${name_pascal}(): Promise<${name_pascal}[]> {\n return this.restClient.get<${name_pascal}[]>(`/${root}/${name_snake}`);\n }"
// }
// // generates a function prototype given an `ActorMethod`
// pub fn ts_client_fn_prototype(method ActorMethod) string {
// name := texttools.name_fix_pascal(method.name)
// params := method.parameters
// .map(content_descriptor_to_parameter(it) or {panic(err)})
// .map(it.typescript())
// .join(', ')
// return_type := content_descriptor_to_parameter(method.result) or {panic(err)}.typ.typescript()
// return 'async ${name}(${params}): Promise<${return_type}> {}'
// }

View File

@@ -0,0 +1,202 @@
module generator
import x.json2 as json
import arrays
import freeflowuniverse.herolib.core.code
import freeflowuniverse.herolib.baobab.specification
import freeflowuniverse.herolib.schemas.openrpc
import freeflowuniverse.herolib.schemas.jsonschema
const specification = specification.ActorSpecification{
name: 'Pet Store'
description: 'A sample API for a pet store'
structure: code.Struct{}
interfaces: [.openapi]
methods: [
specification.ActorMethod{
name: 'listPets'
summary: 'List all pets'
example: openrpc.ExamplePairing{
params: [
openrpc.ExampleRef(openrpc.Example{
name: 'Example limit'
description: 'Example Maximum number of pets to return'
value: 10
})
]
result: openrpc.ExampleRef(openrpc.Example{
name: 'Example response'
value: json.raw_decode('[
{"id": 1, "name": "Fluffy", "tag": "dog"},
{"id": 2, "name": "Whiskers", "tag": "cat"}
]')!
})
}
parameters: [
openrpc.ContentDescriptor{
name: 'limit'
summary: 'Maximum number of pets to return'
description: 'Maximum number of pets to return'
required: false
schema: jsonschema.SchemaRef(jsonschema.Schema{
...jsonschema.schema_u32,
example: 10
})
}
]
result: openrpc.ContentDescriptor{
name: 'pets'
description: 'A paged array of pets'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'array'
items: jsonschema.Items(jsonschema.SchemaRef(jsonschema.Schema{
id: 'pet'
title: 'Pet'
typ: 'object'
properties: {
'id': jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/PetId'
}),
'name': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
}),
'tag': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}
required: ['id', 'name']
}))
})
}
errors: [
openrpc.ErrorSpec{
code: 400
message: 'Invalid request'
}
]
},
specification.ActorMethod{
name: 'createPet'
summary: 'Create a new pet'
example: openrpc.ExamplePairing{
result: openrpc.ExampleRef(openrpc.Example{
name: 'Example response'
value: '[]'
})
}
result: openrpc.ContentDescriptor{
name: 'result'
description: 'The response of the operation.'
required: true
}
errors: [
openrpc.ErrorSpec{
code: 400
message: 'Invalid input'
}
]
},
specification.ActorMethod{
name: 'getPet'
summary: 'Get a pet by ID'
example: openrpc.ExamplePairing{
params: [
openrpc.ExampleRef(openrpc.Example{
name: 'Example petId'
description: 'Example ID of the pet to retrieve'
value: 1
})
]
result: openrpc.ExampleRef(openrpc.Example{
name: 'Example response'
value: json.raw_decode('{"id": 1, "name": "Fluffy", "tag": "dog"}')!
})
}
parameters: [
openrpc.ContentDescriptor{
name: 'petId'
summary: 'ID of the pet to retrieve'
description: 'ID of the pet to retrieve'
required: true
schema: jsonschema.SchemaRef(jsonschema.Schema{
...jsonschema.schema_u32,
format:'uint32'
example: 1
})
}
]
result: openrpc.ContentDescriptor{
name: 'result'
description: 'The response of the operation.'
required: true
schema: jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/Pet'
})
}
errors: [
openrpc.ErrorSpec{
code: 404
message: 'Pet not found'
}
]
},
specification.ActorMethod{
name: 'deletePet'
summary: 'Delete a pet by ID'
example: openrpc.ExamplePairing{
params: [
openrpc.ExampleRef(openrpc.Example{
name: 'Example petId'
description: 'Example ID of the pet to delete'
value: 1
})
]
}
parameters: [
openrpc.ContentDescriptor{
name: 'petId'
summary: 'ID of the pet to delete'
description: 'ID of the pet to delete'
required: true
schema: jsonschema.SchemaRef(jsonschema.Schema{
...jsonschema.schema_u32,
example: 1
})
}
]
result: openrpc.ContentDescriptor{
name: 'result'
description: 'The response of the operation.'
required: true
}
errors: [
openrpc.ErrorSpec{
code: 404
message: 'Pet not found'
}
]
}
]
objects: [
specification.BaseObject{
schema: jsonschema.Schema{
title: 'Pet'
typ: 'object'
properties: {
'id': jsonschema.schema_u32,
'name': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
}),
'tag': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}
required: ['id', 'name']
}
}
]
}
fn test_typescript_client_folder() {
client := typescript_client_folder(specification)
}

View File

@@ -1,7 +1,7 @@
module generator
// pub fn generate_object_code(actor Struct, object BaseObject) VFile {
// obj_name := texttools.name_fix_snake(object.structure.name)
// obj_name := texttools.snake_case(object.structure.name)
// object_type := object.structure.name
// mut items := []CodeItem{}
@@ -41,7 +41,7 @@ module generator
// return a.objects.map(code.new_file(
// mod: texttools.name_fix(a.name)
// name: '${texttools.name_fix(it.structure.name)}_model'
// // imports: [Import{mod:'freeflowuniverse.herolib.baobab.actor'}]
// // imports: [Import{mod:'freeflowuniverse.herolib.baobab.stage'}]
// items: [it.structure]
// ))
// }

View File

@@ -0,0 +1,406 @@
module generator
import freeflowuniverse.herolib.baobab.specification {BaseObject}
import freeflowuniverse.herolib.core.code { type_from_symbol, VFile, CodeItem, Function, Import, Param, Param, Struct, StructField, Type }
import freeflowuniverse.herolib.core.texttools
const id_param = Param{
name: 'id'
typ: type_from_symbol('u32')
}
// pub fn generate_object_code(actor Struct, object BaseObject) VFile {
// obj_name := texttools.name_fix_pascal_to_snake(object.structure.name)
// object_type := object.structure.name
// mut items := []CodeItem{}
// items = [generate_new_method(actor, object), generate_get_method(actor, object),
// generate_set_method(actor, object), generate_delete_method(actor, object),
// generate_list_result_struct(actor, object), generate_list_method(actor, object)]
// items << generate_object_methods(actor, object)
// mut file := code.new_file(
// mod: texttools.name_fix(actor.name)
// name: obj_name
// imports: [
// Import{
// mod: object.structure.mod
// types: [object_type]
// },
// Import{
// mod: 'freeflowuniverse.herolib.baobab.backend'
// types: ['FilterParams']
// },
// ]
// items: items
// )
// if object.structure.fields.any(it.attrs.any(it.name == 'index')) {
// // can't filter without indices
// filter_params := generate_filter_params(actor, object)
// file.items << filter_params.map(CodeItem(it))
// file.items << generate_filter_method(actor, object)
// }
// return file
// }
// // generate_object_methods generates CRUD actor methods for a provided structure
// fn generate_get_method(actor Struct, object BaseObject) Function {
// object_name := texttools.name_fix_pascal_to_snake(object.structure.name)
// object_type := object.structure.name
// get_method := Function{
// name: 'get_${object_name}'
// description: 'gets the ${object_name} with the given object id'
// receiver: Param{
// mutable: true
// name: 'actor'
// typ: type_from_symbol(actor.name)
// }
// }
// params: [generator.id_param]
// result: Param{
// typ: type_from_symbol(object.structure.name)
// is_result: true
// }
// body: 'return actor.backend.get[${object_type}](id)!'
// }
// return get_method
// }
// // generate_object_methods generates CRUD actor methods for a provided structure
// fn generate_set_method(actor Struct, object BaseObject) Function {
// object_name := texttools.name_fix_pascal_to_snake(object.structure.name)
// object_type := object.structure.name
// param_getters := generate_param_getters(
// structure: object.structure
// prefix: ''
// only_mutable: true
// )
// body := 'actor.backend.set[${object_type}](${object_name})!'
// get_method := Function{
// name: 'set_${object_name}'
// description: 'updates the ${object.structure.name} with the given object id'
// receiver: Param{
// mutable: true
// name: 'actor'
// typ: type_from_symbol(actor.name)
// }
// }
// params: [
// Param{
// name: object_name
// typ: Type{
// symbol: object_type
// }
// },
// ]
// result: Param{
// is_result: true
// }
// body: body
// }
// return get_method
// }
// // generate_object_methods generates CRUD actor methods for a provided structure
// fn generate_delete_method(actor Struct, object BaseObject) Function {
// object_name := texttools.name_fix_pascal_to_snake(object.structure.name)
// object_type := object.structure.name
// body := 'actor.backend.delete[${object_type}](id)!'
// get_method := Function{
// name: 'delete_${object_name}'
// description: 'deletes the ${object.structure.name} with the given object id'
// receiver: Param{
// mutable: true
// name: 'actor'
// typ: Type{
// symbol: actor.name
// }
// }
// params: [generator.id_param]
// result: Param{
// is_result: true
// }
// body: body
// }
// return get_method
// }
// // generate_object_methods generates CRUD actor methods for a provided structure
// fn generate_new_method(actor Struct, object BaseObject) Function {
// object_name := texttools.name_fix_pascal_to_snake(object.structure.name)
// object_type := object.structure.name
// param_getters := generate_param_getters(
// structure: object.structure
// prefix: ''
// only_mutable: false
// )
// body := 'return actor.backend.new[${object_type}](${object_name})!'
// new_method := Function{
// name: 'new_${object_name}'
// description: 'news the ${object.structure.name} with the given object id'
// receiver: Param{
// name: 'actor'
// typ: Type{
// symbol: actor.name
// }
// mutable: true
// }
// params: [
// Param{
// name: object_name
// typ: Type{
// symbol: object_type
// }
// },
// ]
// result: Param{
// is_result: true
// typ: Type{
// symbol: 'u32'
// }
// }
// body: body
// }
// return new_method
// }
// // generate_object_methods generates CRUD actor methods for a provided structure
// fn generate_list_result_struct(actor Struct, object BaseObject) Struct {
// object_name := texttools.name_fix_pascal_to_snake(object.structure.name)
// object_type := object.structure.name
// return Struct{
// name: '${object_type}List'
// is_pub: true
// fields: [
// StructField{
// name: 'items'
// typ: Type{
// symbol: '[]${object_type}'
// }
// },
// ]
// }
// }
// // generate_object_methods generates CRUD actor methods for a provided structure
// fn generate_list_method(actor Struct, object BaseObject) Function {
// object_name := texttools.name_fix_pascal_to_snake(object.structure.name)
// object_type := object.structure.name
// list_struct := Struct{
// name: '${object_type}List'
// fields: [
// StructField{
// name: 'items'
// typ: Type{
// symbol: '[]${object_type}'
// }
// },
// ]
// }
// param_getters := generate_param_getters(
// structure: object.structure
// prefix: ''
// only_mutable: false
// )
// body := 'return ${object_type}List{items:actor.backend.list[${object_type}]()!}'
// result_struct := generate_list_result_struct(actor, object)
// mut result := Param{}
// result.typ.symbol = result_struct.name
// result.is_result = true
// new_method := Function{
// name: 'list_${object_name}'
// description: 'lists all of the ${object_name} objects'
// receiver: Param{
// name: 'actor'
// typ: Type{
// symbol: actor.name
// }
// mutable: true
// }
// params: []
// result: result
// body: body
// }
// return new_method
// }
// fn generate_filter_params(actor Struct, object BaseObject) []Struct {
// object_name := texttools.name_fix_pascal_to_snake(object.structure.name)
// object_type := object.structure.name
// return [
// Struct{
// name: 'Filter${object_type}Params'
// fields: [
// StructField{
// name: 'filter'
// typ: Type{
// symbol: '${object_type}Filter'
// }
// },
// StructField{
// name: 'params'
// typ: Type{
// symbol: 'FilterParams'
// }
// },
// ]
// },
// Struct{
// name: '${object_type}Filter'
// fields: object.structure.fields.filter(it.attrs.any(it.name == 'index'))
// },
// ]
// }
// // generate_object_methods generates CRUD actor methods for a provided structure
// fn generate_filter_method(actor Struct, object BaseObject) Function {
// object_name := texttools.name_fix_pascal_to_snake(object.structure.name)
// object_type := object.structure.name
// param_getters := generate_param_getters(
// structure: object.structure
// prefix: ''
// only_mutable: false
// )
// params_type := 'Filter${object_type}Params'
// body := 'return actor.backend.filter[${object_type}, ${object_type}Filter](filter.filter, filter.params)!'
// return Function{
// name: 'filter_${object_name}'
// description: 'lists all of the ${object_name} objects'
// receiver: Param{
// name: 'actor'
// typ: Type{
// symbol: actor.name
// }
// mutable: true
// }
// params: [
// Param{
// name: 'filter'
// typ: Type{
// symbol: params_type
// }
// },
// ]
// result: Param{
// typ: Type{
// symbol: '[]${object_type}'
// }
// is_result: true
// }
// body: body
// }
// }
// // // generate_object_methods generates CRUD actor methods for a provided structure
// // fn generate_object_methods(actor Struct, object BaseObject) []Function {
// // object_name := texttools.name_fix_pascal_to_snake(object.structure.name)
// // object_type := object.structure.name
// // mut funcs := []Function{}
// // for method in object.methods {
// // mut params := [Param{
// // name: 'id'
// // typ: Type{
// // symbol: 'u32'
// // }
// // }]
// // params << method.params
// // funcs << Function{
// // name: method.name
// // description: method.description
// // receiver: Param{
// // name: 'actor'
// // typ: Type{
// // symbol: actor.name
// // }
// // mutable: true
// // }
// // params: params
// // result: method.result
// // body: 'obj := actor.backend.get[${method.receiver.typ.symbol}](id)!
// // obj.${method.name}(${method.params.map(it.name).join(',')})
// // actor.backend.set[${method.receiver.typ.symbol}](obj)!
// // '
// // }
// // }
// // return funcs
// // }
// @[params]
// struct GenerateParamGetters {
// structure Struct
// prefix string
// only_mutable bool // if true generates param.get methods for only mutable struct fields. Used for updating.
// }
// fn generate_param_getters(params GenerateParamGetters) []string {
// mut param_getters := []string{}
// fields := if params.only_mutable {
// params.structure.fields.filter(it.is_mut && it.is_pub)
// } else {
// params.structure.fields.filter(it.is_pub)
// }
// for field in fields {
// if field.typ.symbol.starts_with_capital() {
// subgetters := generate_param_getters(GenerateParamGetters{
// ...params
// structure: field.structure
// prefix: '${field.name}_'
// })
// // name of the tested object, used for param declaration
// // ex: fruits []Fruit becomes fruit_name
// nested_name := field.structure.name.to_lower()
// if field.typ.is_map {
// param_getters.insert(0, '${nested_name}_key := params.get(\'${nested_name}_key\')!')
// param_getters << '${field.name}: {${nested_name}_key: ${field.structure.name}}{'
// } else if field.typ.is_array {
// param_getters << '${field.name}: [${field.structure.name}{'
// } else {
// param_getters << '${field.name}: ${field.structure.name}{'
// }
// param_getters << subgetters
// param_getters << if field.typ.is_array { '}]' } else { '}' }
// continue
// }
// mut get_method := '${field.name}: params.get'
// if field.typ.symbol != 'string' {
// // TODO: check if params method actually exists
// 'get_${field.typ.symbol}'
// }
// if field.default != '' {
// get_method += '_default'
// }
// get_method = get_method + "('${params.prefix}${field.name}')!"
// param_getters << get_method
// }
// return param_getters
// }
// @[params]
// struct GetChildField {
// parent Struct @[required]
// child Struct @[required]
// }
// fn get_child_field(params GetChildField) StructField {
// fields := params.parent.fields.filter(it.typ.symbol == 'map[string]&${params.child.name}')
// if fields.len != 1 {
// panic('this should never happen')
// }
// return fields[0]
// }

View File

@@ -0,0 +1,168 @@
module generator
import freeflowuniverse.herolib.core.code { VFile, CustomCode, Function, Import, Struct }
import freeflowuniverse.herolib.baobab.specification {BaseObject}
import rand
import freeflowuniverse.herolib.core.texttools
// // generate_object_methods generates CRUD actor methods for a provided structure
// pub fn generate_object_test_code(actor Struct, object BaseObject) !VFile {
// consts := CustomCode{"const db_dir = '\${os.home_dir()}/hero/db'
// const actor_name = '${actor.name}_test_actor'"}
// clean_code := 'mut actor := get(name: actor_name)!\nactor.backend.reset()!'
// testsuite_begin := Function{
// name: 'testsuite_begin'
// body: clean_code
// }
// testsuite_end := Function{
// name: 'testsuite_end'
// body: clean_code
// }
// actor_name := texttools.name_fix(actor.name)
// object_name := texttools.name_fix_pascal_to_snake(object.schema.name)
// object_type := object.structure.name
// // TODO: support modules outside of crystal
// mut file := VFile{
// name: '${object_name}_test'
// mod: texttools.name_fix(actor_name)
// imports: [
// Import{
// mod: 'os'
// },
// Import{
// mod: '${object.structure.mod}'
// types: [object_type]
// },
// ]
// items: [
// consts,
// testsuite_begin,
// testsuite_end,
// generate_new_method_test(actor, object)!,
// generate_get_method_test(actor, object)!,
// ]
// }
// if object.structure.fields.any(it.attrs.any(it.name == 'index')) {
// // can't filter without indices
// file.items << generate_filter_test(actor, object)!
// }
// return file
// }
// // generate_object_methods generates CRUD actor methods for a provided structure
// fn generate_new_method_test(actor Struct, object BaseObject) !Function {
// object_name := texttools.name_fix_pascal_to_snake(object.structure.name)
// object_type := object.structure.name
// required_fields := object.structure.fields.filter(it.attrs.any(it.name == 'required'))
// mut fields := []string{}
// for field in required_fields {
// mut field_decl := '${field.name}: ${get_mock_value(field.typ.symbol())!}'
// fields << field_decl
// }
// body := 'mut actor := get(name: actor_name)!
// mut ${object_name}_id := actor.new_${object_name}(${object_type}{${fields.join(',')}})!
// assert ${object_name}_id == 1
// ${object_name}_id = actor.new_${object_name}(${object_type}{${fields.join(',')}})!
// assert ${object_name}_id == 2'
// return Function{
// name: 'test_new_${object_name}'
// description: 'news the ${object_type} with the given object id'
// result: code.Param{
// is_result: true
// }
// body: body
// }
// }
// // generate_object_methods generates CRUD actor methods for a provided structure
// fn generate_get_method_test(actor Struct, object BaseObject) !Function {
// object_name := texttools.name_fix_pascal_to_snake(object.structure.name)
// object_type := object.structure.name
// required_fields := object.structure.fields.filter(it.attrs.any(it.name == 'required'))
// mut fields := []string{}
// for field in required_fields {
// mut field_decl := '${field.name}: ${get_mock_value(field.typ.symbol())!}'
// fields << field_decl
// }
// body := 'mut actor := get(name: actor_name)!
// mut ${object_name} := ${object_type}{${fields.join(',')}}
// ${object_name}.id = actor.new_${object_name}(${object_name})!
// assert ${object_name} == actor.get_${object_name}(${object_name}.id)!'
// return Function{
// name: 'test_get_${object_name}'
// description: 'news the ${object_type} with the given object id'
// result: code.Param{
// is_result: true
// }
// body: body
// }
// }
// // generate_object_methods generates CRUD actor methods for a provided structure
// fn generate_filter_test(actor Struct, object BaseObject) !Function {
// object_name := texttools.name_fix_pascal_to_snake(object.structure.name)
// object_type := object.structure.name
// index_fields := object.structure.fields.filter(it.attrs.any(it.name == 'index'))
// if index_fields.len == 0 {
// return error('Cannot generate filter method test for object without any index fields')
// }
// mut index_tests := []string{}
// for i, field in index_fields {
// val := get_mock_value(field.typ.symbol())!
// index_field := '${field.name}: ${val}' // index field assignment line
// mut fields := [index_field]
// fields << get_required_fields(object.structure)!
// index_tests << '${object_name}_id${i} := actor.new_${object_name}(${object_type}{${fields.join(',')}})!
// ${object_name}_list${i} := actor.filter_${object_name}(
// filter: ${object_type}Filter{${index_field}}
// )!
// assert ${object_name}_list${i}.len == 1
// assert ${object_name}_list${i}[0].${field.name} == ${val}
// '
// }
// body := 'mut actor := get(name: actor_name)!
// \n${index_tests.join('\n\n')}'
// return Function{
// name: 'test_filter_${object_name}'
// description: 'news the ${object_type} with the given object id'
// result: code.Param{
// is_result: true
// }
// body: body
// }
// }
// fn get_required_fields(s Struct) ![]string {
// required_fields := s.fields.filter(it.attrs.any(it.name == 'required'))
// mut fields := []string{}
// for field in required_fields {
// fields << '${field.name}: ${get_mock_value(field.typ.symbol())!}'
// }
// return fields
// }
// fn get_mock_value(typ string) !string {
// if typ == 'string' {
// return "'mock_string_${rand.string(3)}'"
// } else if typ == 'int' || typ == 'u32' {
// return '42'
// } else {
// return error('mock values for types other than strings and numbers are not yet supported')
// }
// }

View File

@@ -0,0 +1,149 @@
module generator
import freeflowuniverse.herolib.core.code { Result, Object, Param, Folder, IFile, VFile, CodeItem, File, Function, Import, Module, Struct, CustomCode }
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.schemas.openrpc {Example, ContentDescriptor}
import freeflowuniverse.herolib.schemas.jsonschema.codegen {schemaref_to_type}
import freeflowuniverse.herolib.baobab.specification {ActorMethod, ActorSpecification}
fn generate_handle_file(spec ActorSpecification) !VFile {
mut items := []CodeItem{}
items << CustomCode{generate_handle_function(spec)}
for method in spec.methods {
items << generate_method_handle(spec.name, method)!
}
return VFile {
name: 'act'
imports: [
Import{mod:'freeflowuniverse.herolib.baobab.stage' types:['Action']}
Import{mod:'freeflowuniverse.herolib.core.texttools'}
Import{mod:'x.json2 as json'}
]
items: items
}
}
pub fn generate_handle_function(spec ActorSpecification) string {
actor_name_pascal := texttools.snake_case_to_pascal(spec.name)
mut operation_handlers := []string{}
mut routes := []string{}
// Iterate over OpenAPI paths and operations
for method in spec.methods {
operation_id := method.name
params := method.parameters.map(it.name).join(', ')
// Generate route case
route := generate_route_case(operation_id, 'handle_${operation_id}')
routes << route
}
// Combine the generated handlers and main router into a single file
return [
'// AUTO-GENERATED FILE - DO NOT EDIT MANUALLY',
'',
'pub fn (mut actor ${actor_name_pascal}Actor) act(action Action) !Action {',
' return match texttools.snake_case(action.name) {',
routes.join('\n'),
' else {',
' return error("Unknown operation: \${action.name}")',
' }',
' }',
'}',
].join('\n')
}
pub fn generate_method_handle(actor_name string, method ActorMethod) !Function {
actor_name_pascal := texttools.snake_case_to_pascal(actor_name)
name_fixed := texttools.snake_case(method.name)
mut body := ''
if method.parameters.len == 1 {
param := method.parameters[0]
param_name := texttools.snake_case(param.name)
decode_stmt := generate_decode_stmt('action.params', param)!
body += '${param_name} := ${decode_stmt}\n'
}
if method.parameters.len > 1 {
body += 'params_arr := json.raw_decode(action.params)!.arr()\n'
for i, param in method.parameters {
param_name := texttools.snake_case(param.name)
decode_stmt := generate_decode_stmt('params_arr[${i}].str()', param)!
body += '${param_name} := ${decode_stmt}'
}
}
call_stmt := generate_call_stmt(actor_name, method)!
body += '${call_stmt}\n'
body += '${generate_return_stmt(method)!}\n'
return Function {
name: 'handle_${name_fixed}'
description: '// Handler for ${name_fixed}\n'
receiver: Param{name: 'actor', mutable: true, typ: Object{'${actor_name_pascal}Actor'}}
params: [Param{name: 'action', typ: Object{'Action'}}]
result: Param{typ: Result{Object{'Action'}}}
body: body
}
}
fn method_is_void(method ActorMethod) !bool {
return schemaref_to_type(method.result.schema).vgen().trim_space() == ''
}
pub fn generate_example_method_handle(actor_name string, method ActorMethod) !Function {
actor_name_pascal := texttools.snake_case_to_pascal(actor_name)
name_fixed := texttools.snake_case(method.name)
body := if !method_is_void(method)! {
if method.example.result is Example {
'return Action{...action, result: json.encode(\'${method.example.result.value}\')}'
} else {
"return action"
}
} else { "return action" }
return Function {
name: 'handle_${name_fixed}_example'
description: '// Handler for ${name_fixed}\n'
receiver: Param{name: 'actor', mutable: true, typ: Object{'${actor_name_pascal}Actor'}}
params: [Param{name: 'action', typ: Object{'Action'}}]
result: Param{typ: Result{Object{'Action'}}}
body: body
}
}
fn generate_call_stmt(name string, method ActorMethod) !string {
mut call_stmt := if schemaref_to_type(method.result.schema).vgen().trim_space() != '' {
'${texttools.snake_case(method.result.name)} := '
} else {''}
method_name := texttools.snake_case(method.name)
snake_name := texttools.snake_case(name)
param_names := method.parameters.map(texttools.snake_case(it.name))
call_stmt += 'actor.${snake_name}.${method_name}(${param_names.join(", ")})!'
return call_stmt
}
fn generate_return_stmt(method ActorMethod) !string {
if schemaref_to_type(method.result.schema).vgen().trim_space() != '' {
return 'return Action{...action, result: json.encode(${texttools.snake_case(method.result.name)})}'
}
return "return action"
}
// Helper function to generate a case block for the main router
fn generate_route_case(case string, handler_name string) string {
name_fixed := texttools.snake_case(handler_name)
return "'${texttools.snake_case(case)}' {actor.${name_fixed}(action)}"
}
// generates decode statement for variable with given name
fn generate_decode_stmt(name string, param ContentDescriptor) !string {
param_type := schemaref_to_type(param.schema)
if param_type is Object {
return 'json.decode[${schemaref_to_type(param.schema).vgen()}](${name})!'
}
else if param_type is code.Array {
return 'json.decode[${schemaref_to_type(param.schema).vgen()}](${name})'
}
param_symbol := param_type.vgen()
return if param_symbol == 'string' {
'${name}.str()'
} else {'${name}.${param_type.vgen()}()'}
}

View File

@@ -0,0 +1,82 @@
module generator
import freeflowuniverse.herolib.core.code { Folder, IFolder, IFile, VFile, CodeItem, File, Function, Import, Module, Struct, CustomCode }
import freeflowuniverse.herolib.schemas.openapi
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.baobab.specification {ActorMethod, ActorSpecification, ActorInterface}
import json
@[params]
pub struct Params {
pub:
interfaces []ActorInterface // the interfaces to be supported
}
pub fn generate_actor_folder(spec ActorSpecification, params Params) !Folder {
mut files := []IFile{}
mut folders := []IFolder{}
files = [generate_readme_file(spec)!]
mut docs_files := []IFile{}
mut spec_files := []IFile{}
// generate code files for supported interfaces
for iface in params.interfaces {
match iface {
.openrpc {
// convert actor spec to openrpc spec
openrpc_spec := spec.to_openrpc()
spec_files << generate_openrpc_file(openrpc_spec)!
}
.openapi {
// convert actor spec to openrpc spec
openapi_spec_raw := spec.to_openapi()
spec_files << generate_openapi_file(openapi_spec_raw)!
openapi_spec := openapi.process(openapi_spec_raw)!
folders << generate_openapi_ts_client(openapi_spec)!
}
else {}
}
}
specs_folder := Folder {
name: 'specs'
files: spec_files
}
// folder with docs
folders << Folder {
name: 'docs'
files: docs_files
folders: [specs_folder]
}
folders << generate_scripts_folder(spec.name, false)
folders << generate_examples_folder()!
// create module with code files and docs folder
name_fixed := texttools.snake_case(spec.name)
return code.Folder{
name: '${name_fixed}'
files: files
folders: folders
modules: [generate_actor_module(spec, params)!]
}
}
fn generate_readme_file(spec ActorSpecification) !File {
return File{
name: 'README'
extension: 'md'
content: '# ${spec.name}\n${spec.description}'
}
}
pub fn generate_examples_folder() !Folder {
return Folder {
name: 'examples'
}
}

View File

@@ -0,0 +1,106 @@
module generator
import freeflowuniverse.herolib.core.code { Folder, IFolder, IFile, VFile, CodeItem, File, Function, Import, Module, Struct, CustomCode }
import freeflowuniverse.herolib.schemas.openapi
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.baobab.specification {ActorMethod, ActorSpecification, ActorInterface}
import json
pub fn generate_actor_module(spec ActorSpecification, params Params) !Module {
mut files := []IFile{}
mut folders := []IFolder{}
files = [
generate_actor_file(spec)!,
generate_actor_test_file(spec)!,
generate_specs_file(spec.name, params.interfaces)!,
generate_handle_file(spec)!,
generate_methods_file(spec)!
generate_methods_interface_file(spec)!
generate_methods_example_file(spec)!
generate_client_file(spec)!
generate_model_file(spec)!
]
// generate code files for supported interfaces
for iface in params.interfaces {
match iface {
.openrpc {
// convert actor spec to openrpc spec
openrpc_spec := spec.to_openrpc()
iface_file, iface_test_file := generate_openrpc_interface_files(params.interfaces)
files << iface_file
files << iface_test_file
}
.openapi {
// convert actor spec to openrpc spec
openapi_spec_raw := spec.to_openapi()
openapi_spec := openapi.process(openapi_spec_raw)!
// generate openrpc code files
iface_file, iface_test_file := generate_openapi_interface_files(params.interfaces)
files << iface_file
files << iface_test_file
}
.http {
// interfaces that have http controllers
controllers := params.interfaces.filter(it == .openrpc || it == .openapi)
// generate openrpc code files
iface_file, iface_test_file := generate_http_interface_files(controllers)
files << iface_file
files << iface_test_file
}
.command {
files << generate_command_file(spec)!
}
else {
return error('unsupported interface ${iface}')
}
}
}
// create module with code files and docs folder
name_fixed := texttools.snake_case(spec.name)
return code.new_module(
name: '${name_fixed}'
description: spec.description
files: files
folders: folders
in_src: true
)
}
fn generate_actor_file(spec ActorSpecification) !VFile {
dollar := '$'
version := spec.version
name_snake := texttools.snake_case(spec.name)
name_pascal := texttools.snake_case_to_pascal(spec.name)
actor_code := $tmpl('./templates/actor.v.template')
return VFile {
name: 'actor'
items: [CustomCode{actor_code}]
}
}
fn generate_actor_test_file(spec ActorSpecification) !VFile {
dollar := '$'
actor_name_snake := texttools.snake_case(spec.name)
actor_name_pascal := texttools.snake_case_to_pascal(spec.name)
actor_test_code := $tmpl('./templates/actor_test.v.template')
return VFile {
name: 'actor_test'
items: [CustomCode{actor_test_code}]
}
}
fn generate_specs_file(name string, interfaces []ActorInterface) !VFile {
support_openrpc := ActorInterface.openrpc in interfaces
support_openapi := ActorInterface.openapi in interfaces
dollar := '$'
actor_name_snake := texttools.snake_case(name)
actor_name_pascal := texttools.snake_case_to_pascal(name)
actor_code := $tmpl('./templates/specifications.v.template')
return VFile {
name: 'specifications'
items: [CustomCode{actor_code}]
}
}

View File

@@ -0,0 +1,273 @@
module generator
import freeflowuniverse.herolib.core.code
import freeflowuniverse.herolib.baobab.specification
import freeflowuniverse.herolib.schemas.openrpc
import freeflowuniverse.herolib.schemas.jsonschema
import os
import x.json2 as json {Any}
const actor_spec = specification.ActorSpecification{
name: 'Pet Store'
description: 'A sample API for a pet store'
structure: code.Struct{}
interfaces: [.openapi]
methods: [
specification.ActorMethod{
name: 'listPets'
summary: 'List all pets'
example: openrpc.ExamplePairing{
params: [
openrpc.ExampleRef(openrpc.Example{
name: 'Example limit'
description: 'Example Maximum number of pets to return'
value: 10
})
]
result: openrpc.ExampleRef(openrpc.Example{
name: 'Example response'
value: json.raw_decode('[
{"id": 1, "name": "Fluffy", "tag": "dog"},
{"id": 2, "name": "Whiskers", "tag": "cat"}
]')!
})
}
parameters: [
openrpc.ContentDescriptor{
name: 'limit'
summary: 'Maximum number of pets to return'
description: 'Maximum number of pets to return'
required: false
schema: jsonschema.SchemaRef(jsonschema.Schema{
...jsonschema.schema_u32,
example: 10
})
}
]
result: openrpc.ContentDescriptor{
name: 'pets'
description: 'A paged array of pets'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'array'
items: jsonschema.Items(jsonschema.SchemaRef(jsonschema.Schema{
id: 'pet'
title: 'Pet'
typ: 'object'
properties: {
'id': jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/PetId'
}),
'name': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
}),
'tag': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}
required: ['id', 'name']
}))
})
}
errors: [
openrpc.ErrorSpec{
code: 400
message: 'Invalid request'
}
]
},
specification.ActorMethod{
name: 'newPet'
summary: 'Create a new pet'
parameters: [
openrpc.ContentDescriptor{
name: 'result'
description: 'The response of the operation.'
required: true
schema: jsonschema.SchemaRef(jsonschema.Schema{
id: 'pet'
title: 'Pet'
typ: 'object'
properties: {
'id': jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/PetId'
}),
'name': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
}),
'tag': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}
required: ['id', 'name']
})
}
]
example: openrpc.ExamplePairing{
result: openrpc.ExampleRef(openrpc.Example{
name: 'Example response'
value: '[]'
})
}
result: openrpc.ContentDescriptor{
name: 'petId'
summary: 'ID of the created pet'
description: 'ID of the created pet'
required: true
schema: jsonschema.SchemaRef(jsonschema.Schema{
...jsonschema.schema_u32,
example: 1
})
}
errors: [
openrpc.ErrorSpec{
code: 400
message: 'Invalid input'
}
]
},
specification.ActorMethod{
name: 'getPet'
summary: 'Get a pet by ID'
example: openrpc.ExamplePairing{
params: [
openrpc.ExampleRef(openrpc.Example{
name: 'Example petId'
description: 'Example ID of the pet to retrieve'
value: 1
})
]
result: openrpc.ExampleRef(openrpc.Example{
name: 'Example response'
value: json.raw_decode('{"id": 1, "name": "Fluffy", "tag": "dog"}')!
})
}
parameters: [
openrpc.ContentDescriptor{
name: 'petId'
summary: 'ID of the pet to retrieve'
description: 'ID of the pet to retrieve'
required: true
schema: jsonschema.SchemaRef(jsonschema.Schema{
...jsonschema.schema_u32,
format:'uint32'
example: 1
})
}
]
result: openrpc.ContentDescriptor{
name: 'result'
description: 'The response of the operation.'
required: true
schema: jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/Pet'
})
}
errors: [
openrpc.ErrorSpec{
code: 404
message: 'Pet not found'
}
]
},
specification.ActorMethod{
name: 'deletePet'
summary: 'Delete a pet by ID'
example: openrpc.ExamplePairing{
params: [
openrpc.ExampleRef(openrpc.Example{
name: 'Example petId'
description: 'Example ID of the pet to delete'
value: 1
})
]
}
parameters: [
openrpc.ContentDescriptor{
name: 'petId'
summary: 'ID of the pet to delete'
description: 'ID of the pet to delete'
required: true
schema: jsonschema.SchemaRef(jsonschema.Schema{
...jsonschema.schema_u32,
example: 1
})
}
]
result: openrpc.ContentDescriptor{
name: 'result'
description: 'The response of the operation.'
required: true
}
errors: [
openrpc.ErrorSpec{
code: 404
message: 'Pet not found'
}
]
}
]
objects: [
specification.BaseObject{
schema: jsonschema.Schema{
title: 'Pet'
typ: 'object'
properties: {
'id': jsonschema.schema_u32,
'name': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
}),
'tag': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}
required: ['id', 'name']
}
}
]
}
const destination = '${os.dir(@FILE)}/testdata'
fn test_generate_plain_actor_module() {
// plain actor module without interfaces
actor_module := generate_actor_module(actor_spec)!
actor_module.write(destination,
format: true
overwrite: true
test: true
)!
}
fn test_generate_actor_module_with_openrpc_interface() {
// plain actor module without interfaces
actor_module := generate_actor_module(actor_spec, interfaces: [.openrpc])!
actor_module.write(destination,
format: true
overwrite: true
test: true
)!
}
fn test_generate_actor_module_with_openapi_interface() {
// plain actor module without interfaces
actor_module := generate_actor_module(actor_spec,
interfaces: [.openapi]
)!
actor_module.write(destination,
format: true
overwrite: true
test: true
)!
}
fn test_generate_actor_module_with_all_interfaces() {
// plain actor module without interfaces
actor_module := generate_actor_module(actor_spec,
interfaces: [.openapi, .openrpc, .http]
)!
actor_module.write(destination,
format: true
overwrite: true
test: true
)!
}

View File

@@ -0,0 +1,133 @@
module generator
import freeflowuniverse.herolib.core.code { Param, Folder, IFile, VFile, CodeItem, File, Function, Import, Module, Struct, CustomCode, Result }
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.schemas.jsonschema.codegen as jsonschema_codegen {schemaref_to_type}
import freeflowuniverse.herolib.schemas.openrpc.codegen {content_descriptor_to_parameter}
import freeflowuniverse.herolib.baobab.specification {ActorMethod, ActorSpecification}
pub fn generate_client_file(spec ActorSpecification) !VFile {
actor_name_snake := texttools.snake_case(spec.name)
actor_name_pascal := texttools.snake_case_to_pascal(spec.name)
mut items := []CodeItem{}
items << CustomCode {'
pub struct Client {
stage.Client
}
fn new_client(config stage.ActorConfig) !Client {
return Client {
Client: stage.new_client(config)!
}
}'}
for method in spec.methods {
items << generate_client_method(method)!
}
return VFile {
imports: [
Import{
mod: 'freeflowuniverse.herolib.baobab.stage'
},
Import{
mod: 'freeflowuniverse.herolib.core.redisclient'
},
Import{
mod: 'x.json2 as json'
types: ['Any']
}
]
name: 'client_actor'
items: items
}
}
pub fn generate_example_client_file(spec ActorSpecification) !VFile {
actor_name_snake := texttools.snake_case(spec.name)
actor_name_pascal := texttools.snake_case_to_pascal(spec.name)
mut items := []CodeItem{}
items << CustomCode {'
pub struct Client {
stage.Client
}
fn new_client() !Client {
mut redis := redisclient.new(\'localhost:6379\')!
mut rpc_q := redis.rpc_get(\'actor_example_\${name}\')
return Client{
rpc: rpc_q
}
}'}
for method in spec.methods {
items << generate_client_method(method)!
}
return VFile {
imports: [
Import{
mod: 'freeflowuniverse.herolib.baobab.stage'
},
Import{
mod: 'freeflowuniverse.herolib.core.redisclient'
},
Import{
mod: 'x.json2 as json'
types: ['Any']
}
]
name: 'client'
items: items
}
}
pub fn generate_client_method(method ActorMethod) !Function {
name_fixed := texttools.snake_case(method.name)
call_params := if method.parameters.len > 0 {
method.parameters.map(texttools.snake_case(it.name)).map('Any(${it}.str())').join(', ')
} else {''}
params_stmt := if method.parameters.len == 0 {
''
} else if method.parameters.len == 1 {
'params := json.encode(${texttools.snake_case(method.parameters[0].name)})'
} else {
'mut params_arr := []Any{}
params_arr = [${call_params}]
params := json.encode(params_arr.str())
'
}
mut client_call_stmt := "action := client.call_to_action(
name: '${name_fixed}'"
if params_stmt != '' {
client_call_stmt += 'params: params'
}
client_call_stmt += ')!'
result_type := schemaref_to_type(method.result.schema).vgen().trim_space()
result_stmt := if result_type == '' {
''
} else {
"return json.decode[${result_type}](action.result)!"
}
result_param := content_descriptor_to_parameter(method.result)!
return Function {
receiver: code.new_param(v: 'mut client Client')!
result: Param{...result_param, typ: Result{result_param.typ}}
name: name_fixed
body: '${params_stmt}\n${client_call_stmt}\n${result_stmt}'
summary: method.summary
description: method.description
params: method.parameters.map(content_descriptor_to_parameter(it)!)
}
}

View File

@@ -1,8 +1,8 @@
module generator
import freeflowuniverse.herolib.core.code { CodeItem, CustomCode, Import, VFile }
import freeflowuniverse.herolib.core.code { Folder, IFile, VFile, CodeItem, File, Function, Import, Module, Struct, CustomCode }
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.hero.baobab.specification { ActorMethod, ActorSpecification }
import freeflowuniverse.herolib.baobab.specification {ActorMethod, ActorSpecification}
pub fn generate_command_file(spec ActorSpecification) !VFile {
mut items := []CodeItem{}
@@ -26,14 +26,13 @@ pub fn generate_command_file(spec ActorSpecification) !VFile {
}
pub fn generate_cmd_function(spec ActorSpecification) string {
actor_name_snake := texttools.name_fix_snake(spec.name)
actor_name_snake := texttools.snake_case(spec.name)
mut cmd_function := "
pub fn cmd() Command {
mut cmd := Command{
name: '${actor_name_snake}'
usage: ''
description: '${spec.description}'
execute: cmd_execute
}
"
@@ -48,7 +47,7 @@ pub fn generate_cmd_function(spec ActorSpecification) string {
}
pub fn generate_method_cmd(method ActorMethod) string {
method_name_snake := texttools.name_fix_snake(method.name)
method_name_snake := texttools.snake_case(method.name)
return "
mut cmd_${method_name_snake} := Command{
sort_flags: true
@@ -63,16 +62,16 @@ pub fn generate_method_cmd_function(actor_name string, method ActorMethod) strin
mut operation_handlers := []string{}
mut routes := []string{}
actor_name_snake := texttools.name_fix_snake(actor_name)
method_name_snake := texttools.name_fix_snake(method.name)
method_call := if method.func.result.typ.symbol == '' {
actor_name_snake := texttools.snake_case(actor_name)
method_name_snake := texttools.snake_case(method.name)
method_call := if method.result.name == '' {
'${actor_name_snake}.${method_name_snake}()!'
} else {
'result := ${actor_name_snake}.${method_name_snake}()!'
}
return '
fn cmd_${method_name_snake}(cmd Command) ! {
fn cmd_${method_name_snake}_execute(cmd Command) ! {
${method_call}
}
'

View File

@@ -0,0 +1,48 @@
module generator
import freeflowuniverse.herolib.baobab.specification {ActorInterface}
import freeflowuniverse.herolib.core.code { Folder, IFile, VFile, CodeItem, File, Function, Import, Module, Struct, CustomCode }
fn generate_openrpc_interface_files(interfaces []ActorInterface) (VFile, VFile) {
http := ActorInterface.http in interfaces
iface_file := VFile {
name: 'interface_openrpc'
items: [CustomCode{$tmpl('./templates/interface_openrpc.v.template')}]
}
iface_test_file := VFile {
name: 'interface_openrpc_test'
items: [CustomCode{$tmpl('./templates/interface_openrpc_test.v.template')}]
}
return iface_file, iface_test_file
}
fn generate_openapi_interface_files(interfaces []ActorInterface) (VFile, VFile) {
http := ActorInterface.http in interfaces
dollar := '$'
iface_file := VFile {
name: 'interface_openapi'
items: [CustomCode{$tmpl('./templates/interface_openapi.v.template')}]
}
iface_test_file := VFile {
name: 'interface_openapi_test'
items: [CustomCode{$tmpl('./templates/interface_openapi_test.v.template')}]
}
return iface_file, iface_test_file
}
fn generate_http_interface_files(controllers []ActorInterface) (VFile, VFile) {
dollar := '$'
openapi := ActorInterface.openapi in controllers
openrpc := ActorInterface.openrpc in controllers
iface_file := VFile {
name: 'interface_http'
items: [CustomCode{$tmpl('./templates/interface_http.v.template')}]
}
iface_test_file := VFile {
name: 'interface_http_test'
items: [CustomCode{$tmpl('./templates/interface_http_test.v.template')}]
}
return iface_file, iface_test_file
}

View File

@@ -0,0 +1,121 @@
module generator
import freeflowuniverse.herolib.core.code { Array, Folder, IFile, VFile, CodeItem, File, Function, Param, Import, Module, Struct, CustomCode, Result }
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.schemas.openrpc {ContentDescriptor}
import freeflowuniverse.herolib.schemas.openrpc.codegen {content_descriptor_to_parameter, content_descriptor_to_struct}
import freeflowuniverse.herolib.schemas.jsonschema {Schema}
import freeflowuniverse.herolib.schemas.jsonschema.codegen as jsonschema_codegen {schema_to_struct}
import freeflowuniverse.herolib.baobab.specification {ActorMethod, ActorSpecification}
const crud_prefixes = ['new', 'get', 'set', 'delete', 'list']
pub fn generate_methods_file(spec ActorSpecification) !VFile {
name_snake := texttools.snake_case(spec.name)
actor_name_pascal := texttools.snake_case_to_pascal(spec.name)
receiver := generate_methods_receiver(spec.name)
receiver_param := Param {
mutable: true
name: name_snake[0].ascii_str() // receiver is first letter of domain
typ: code.Result{code.Object{receiver.name}}
}
mut items := [CodeItem(receiver), CodeItem(generate_core_factory(receiver_param))]
for method in spec.methods {
items << generate_method_code(receiver_param, ActorMethod{...method, category: spec.method_type(method)})!
}
return VFile {
name: 'methods'
imports: [Import{mod: 'freeflowuniverse.herolib.baobab.osis', types: ['OSIS']}]
items: items
}
}
fn generate_methods_receiver(name string) code.Struct {
return code.Struct {
is_pub: true
name: '${texttools.pascal_case(name)}'
fields: [code.StructField{is_mut: true, name: 'osis', typ:code.Object{'OSIS'}}]
}
}
fn generate_core_factory(receiver code.Param) code.Function {
return code.Function {
is_pub: true
name: 'new_${receiver.typ.symbol()}'
body: "return ${receiver.typ.symbol().trim_left('!?')}{osis: osis.new()!}"
result: receiver
}
}
// returns bodyless method prototype
pub fn generate_method_code(receiver code.Param, method ActorMethod) ![]CodeItem {
result_param := content_descriptor_to_parameter(method.result)!
mut method_code := []CodeItem{}
// TODO: document assumption
obj_params := method.parameters.filter(if it.schema is Schema {it.schema.typ == 'object'} else {false}).map(content_descriptor_to_struct(it))
if obj_param := obj_params[0] {
method_code << obj_param
}
// check if method is a Base Object CRUD Method and
// if so generate the method's body
body := match method.category {
.base_object_new { base_object_new_body(receiver, method)! }
.base_object_get { base_object_get_body(receiver, method)! }
.base_object_set { base_object_set_body(receiver, method)! }
.base_object_delete { base_object_delete_body(receiver, method)! }
.base_object_list { base_object_list_body(receiver, method)! }
else {"panic('implement')"}
}
fn_prototype := generate_method_prototype(receiver, method)!
method_code << Function{
...fn_prototype
body: body
}
return method_code
}
// returns bodyless method prototype
pub fn generate_method_prototype(receiver code.Param, method ActorMethod) !Function {
result_param := content_descriptor_to_parameter(method.result)!
return Function{
name: texttools.snake_case(method.name)
receiver: receiver
result: Param {...result_param, typ: Result{result_param.typ}}
summary: method.summary
description: method.description
params: method.parameters.map(content_descriptor_to_parameter(it)!)
}
}
fn base_object_new_body(receiver Param, method ActorMethod) !string {
parameter := content_descriptor_to_parameter(method.parameters[0])!
return 'return ${receiver.name}.osis.new[${parameter.typ.vgen()}](${texttools.snake_case(parameter.name)})!'
}
fn base_object_get_body(receiver Param, method ActorMethod) !string {
parameter := content_descriptor_to_parameter(method.parameters[0])!
result := content_descriptor_to_parameter(method.result)!
return 'return ${receiver.name}.osis.get[${result.typ.vgen()}](${texttools.snake_case(parameter.name)})!'
}
fn base_object_set_body(receiver Param, method ActorMethod) !string {
parameter := content_descriptor_to_parameter(method.parameters[0])!
return 'return ${receiver.name}.osis.set[${parameter.typ.vgen()}](${parameter.name})!'
}
fn base_object_delete_body(receiver Param, method ActorMethod) !string {
parameter := content_descriptor_to_parameter(method.parameters[0])!
return '${receiver.name}.osis.delete(${texttools.snake_case(parameter.name)})!'
}
fn base_object_list_body(receiver Param, method ActorMethod) !string {
result := content_descriptor_to_parameter(method.result)!
base_object_type := (result.typ as Array).typ
return 'return ${receiver.name}.osis.list[${base_object_type.symbol()}]()!'
}

View File

@@ -0,0 +1,84 @@
module generator
import freeflowuniverse.herolib.core.code { Array, Folder, IFile, VFile, CodeItem, File, Function, Param, Import, Module, Struct, CustomCode, Result }
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.schemas.openrpc {Example}
import freeflowuniverse.herolib.schemas.jsonschema {Schema}
import freeflowuniverse.herolib.schemas.jsonschema.codegen as jsonschema_codegen {schema_to_struct}
import freeflowuniverse.herolib.schemas.openrpc.codegen {content_descriptor_to_parameter}
import freeflowuniverse.herolib.baobab.specification {ActorMethod, ActorSpecification}
pub fn generate_methods_example_file(spec ActorSpecification) !VFile {
name_snake := texttools.snake_case(spec.name)
name_pascal := texttools.pascal_case(spec.name)
receiver := generate_example_methods_receiver(spec.name)
receiver_param := Param {
mutable: true
name: name_snake[0].ascii_str()
typ: code.Result{code.Object{receiver.name}}
}
mut items := [CodeItem(receiver), CodeItem(generate_core_example_factory(receiver_param))]
for method in spec.methods {
items << generate_method_example_code(receiver_param, ActorMethod{...method, category: spec.method_type(method)})!
}
return VFile {
name: 'methods_example'
imports: [
Import{mod: 'freeflowuniverse.herolib.baobab.osis', types: ['OSIS']},
Import{mod: 'x.json2 as json'}
]
items: items
}
}
fn generate_core_example_factory(receiver code.Param) code.Function {
return code.Function {
is_pub: true
name: 'new_${texttools.snake_case(receiver.typ.symbol())}'
body: "return ${receiver.typ.symbol().trim_left('!?')}{OSIS: osis.new()!}"
result: receiver
}
}
fn generate_example_methods_receiver(name string) code.Struct {
return code.Struct {
is_pub: true
name: '${texttools.pascal_case(name)}Example'
embeds: [code.Struct{name:'OSIS'}]
}
}
// returns bodyless method prototype
pub fn generate_method_example_code(receiver code.Param, method ActorMethod) ![]CodeItem {
result_param := content_descriptor_to_parameter(method.result)!
mut method_code := []CodeItem{}
// TODO: document assumption
// obj_params := method.parameters.filter(if it.schema is Schema {it.schema.typ == 'object'} else {false}).map(schema_to_struct(it.schema as Schema))
// if obj_param := obj_params[0] {
// method_code << Struct{...obj_param, name: method.name}
// }
// check if method is a Base Object CRUD Method and
// if so generate the method's body
body := if !method_is_void(method)! {
if method.example.result is Example {
"json_str := '${method.example.result.value}'
return ${generate_decode_stmt('json_str', method.result)!}"
} else {
"return ${result_param.typ.empty_value()}"
}
} else {
""
}
fn_prototype := generate_method_prototype(receiver, method)!
method_code << Function{
...fn_prototype
body: body
}
return method_code
}

View File

@@ -0,0 +1,31 @@
module generator
import freeflowuniverse.herolib.core.code { Array, Folder, IFile, VFile, CodeItem, File, Function, Param, Import, Module, Struct, CustomCode, Result }
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.schemas.openrpc.codegen {content_descriptor_to_parameter}
import freeflowuniverse.herolib.baobab.specification {ActorMethod, ActorSpecification}
pub fn generate_methods_interface_file(spec ActorSpecification) !VFile {
return VFile {
name: 'methods_interface'
imports: [Import{mod: 'freeflowuniverse.herolib.baobab.osis', types: ['OSIS']}]
items: [code.CodeItem(generate_methods_interface_declaration(spec)!)]
}
}
// returns bodyless method prototype
pub fn generate_methods_interface_declaration(spec ActorSpecification) !code.Interface {
name_snake := texttools.snake_case(spec.name)
name_pascal := texttools.pascal_case(spec.name)
receiver := generate_methods_receiver(spec.name)
receiver_param := Param {
mutable: true
name: name_snake[0].ascii_str()
typ: code.Object{receiver.name}
}
return code.Interface {
is_pub: true
name: 'I${name_pascal}'
methods: spec.methods.map(generate_method_prototype(receiver_param, it)!)
}
}

View File

@@ -0,0 +1,19 @@
module generator
import freeflowuniverse.herolib.core.code { Folder, IFile, VFile, CodeItem, File, Function, Param, Import, Module, Struct, CustomCode }
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.schemas.jsonschema.codegen {schema_to_struct}
import freeflowuniverse.herolib.baobab.specification {ActorMethod, ActorSpecification}
pub fn generate_model_file(spec ActorSpecification) !VFile {
actor_name_snake := texttools.snake_case(spec.name)
actor_name_pascal := texttools.snake_case_to_pascal(spec.name)
return VFile {
name: 'model'
items: spec.objects.map(CodeItem(
Struct {...schema_to_struct(it.schema)
is_pub: true
}))
}
}

View File

@@ -0,0 +1,158 @@
module generator
import json
import freeflowuniverse.herolib.core.code { VFile, File, Folder, Function, Module, Struct }
import freeflowuniverse.herolib.schemas.openapi { Components, OpenAPI, Operation }
import freeflowuniverse.herolib.schemas.openapi.codegen
import freeflowuniverse.herolib.schemas.jsonschema.codegen as jsonschema_codegen {schema_to_type}
import net.http
pub fn generate_openapi_file(specification OpenAPI) !File {
openapi_json := specification.encode_json()
return File{
name: 'openapi'
extension: 'json'
content: openapi_json
}
}
pub fn generate_openapi_ts_client(specification OpenAPI) !Folder {
return codegen.ts_client_folder(specification,
body_generator: body_generator
custom_client_code: ' private restClient: HeroRestClient;
constructor(heroKeysClient: any, debug: boolean = true) {
this.restClient = new HeroRestClient(heroKeysClient, debug);
}
'
)!
}
fn body_generator(op openapi.Operation, path_ string, method http.Method) string {
path := path_.replace('{','\${')
return match method {
.post {
if schema := op.payload_schema() {
symbol := schema_to_type(schema).typescript()
"return this.restClient.post<${symbol}>('${path}', data);"
} else {''}
}
.get {
if schema := op.response_schema() {
// if op.params.len
symbol := schema_to_type(schema).typescript()
"return this.restClient.get<${symbol}>('${path}', data);"
} else {''}
} else {''}
}
// return if operation_is_base_object_method(op) {
// bo_method := operation_to_base_object_method(op)
// match method_type(op) {
// .new { ts_client_new_body(op, path) }
// .get { ts_client_get_body(op, path) }
// .set { ts_client_set_body(op, path) }
// .delete { ts_client_delete_body(op, path) }
// .list { ts_client_list_body(op, path) }
// else {''}
// }
// } else {''}
}
// pub fn operation_is_base_object_method(op openapi.Operation, base_objs []string) BaseObjectMethod {
// // name := texttools.pascal_case(op.operation_id)
// // if op.operation_id.starts_with('new') {
// // if op.&& operation.params.len == 1
// return true
// }
// pub fn operation_to_base_object_method(op openapi.Operation) BaseObjectMethod {
// if op.operation_id.starts_with('update')
// }
// pub fn openapi_ts_client_body(op openapi.Operation, path string, method http.Method) string {
// match method {
// post {
// if schema := op.payload_schema() {
// symbol := schema_to_type(schema).typescript()
// return "return this.restClient.post<${symbol}>('${path}', data);"
// }
// }
// }
// return if operation_is_base_object_method(op) {
// bo_method := operation_to_base_object_method(op)
// match bo_method. {
// .new { ts_client_new_body(op, path) }
// .get { ts_client_get_body(op, path) }
// .set { ts_client_set_body(op, path) }
// .delete { ts_client_delete_body(op, path) }
// .list { ts_client_list_body(op, path) }
// else {''}
// }
// } else {''}
// }
fn get_endpoint(path string) string {
return if path == '' {
''
} else {
"/${path.trim('/')}"
}
}
// // generates a Base Object's `create` method
// fn ts_client_new_body(op Operation, path string) string {
// // the parameter of a base object new method is always the base object
// bo_param := openapi_codegen.parameter_to_param(op.parameters[0])
// return "return this.restClient.post<${bo_param.typ.typescript()}>('${get_endpoint(path)}', ${bo_param.name});"
// }
// // generates a Base Object's `create` method
// fn ts_client_get_body(op Operation, path string) string {
// // the parameter of a base object get method is always the id
// id_param := openapi_codegen.parameter_to_param(op.parameters[0])
// return "return this.restClient.get<${id_param.typ.typescript()}>('${get_endpoint(path)}', ${id_param.name});"
// }
// // generates a Base Object's `create` method
// fn ts_client_set_body(op Operation, path string) string {
// // the parameter of a base object set method is always the base object
// bo_param := openapi_codegen.parameter_to_param(op.parameters[0])
// return "return this.restClient.put<${bo_param.typ.typescript()}>('${get_endpoint(path)}', ${bo_param.name});"
// }
// // generates a Base Object's `delete` method
// fn ts_client_delete_body(op Operation, path string) string {
// // the parameter of a base object delete method is always the id
// id_param := openapi_codegen.parameter_to_param(op.parameters[0])
// return "return this.restClient.get<${id_param.typ.typescript()}>('${get_endpoint(path)}', ${id_param.name});"
// }
// // generates a Base Object's `list` method
// fn ts_client_list_body(op Operation, path string) string {
// // the result parameter of a base object list method is always the array of bo
// result_param := openapi_codegen.parameter_to_param(op.parameters[0])
// return "return this.restClient.get<${result_param.typ.typescript()}>('${get_endpoint(path)}');"
// }
// pub enum BaseObjectMethodType {
// new
// get
// set
// delete
// list
// other
// }
// pub struct BaseObjectMethod {
// pub:
// typ BaseObjectMethodType
// object string // the name of the base object
// }

View File

@@ -1,12 +1,9 @@
module generator
import json
import freeflowuniverse.herolib.core.code { File, Function, Struct, VFile }
import freeflowuniverse.herolib.core.pathlib
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.hero.baobab.specification
import freeflowuniverse.herolib.rpc.openrpc { OpenRPC }
import freeflowuniverse.herolib.data.jsonschema
import freeflowuniverse.herolib.core.code { VFile, File, Function, Module, Struct }
import freeflowuniverse.herolib.schemas.openrpc { Components, OpenRPC }
import freeflowuniverse.herolib.schemas.openrpc.codegen { generate_client_file, generate_client_test_file }
pub fn generate_openrpc_file(spec OpenRPC) !File {
return File{
@@ -21,9 +18,9 @@ pub fn generate_openrpc_client_file(spec OpenRPC) !VFile {
// for object in spec.objects {
// objects_map[object.structure.name] = object.structure
// }
client_file := spec.generate_client_file(objects_map)!
return VFile{
...client_file
client_file := generate_client_file(spec, objects_map)!
return VFile {
...client_file,
name: 'client_openrpc'
}
}
@@ -37,9 +34,9 @@ pub fn generate_openrpc_client_test_file(spec OpenRPC) !VFile {
// for method in spec.methods {
// methods_map[method.func.name] = method.func
// }
file := spec.generate_client_test_file(methods_map, objects_map)!
return VFile{
...file
file := generate_client_test_file(spec, methods_map, objects_map)!
return VFile {
...file,
name: 'client_openrpc_test'
}
}

View File

@@ -1,7 +1,7 @@
module generator
import freeflowuniverse.herolib.core.code { Function, Param, Result, Struct, Type }
import freeflowuniverse.herolib.rpc.openrpc
import freeflowuniverse.herolib.schemas.openrpc
const test_actor_specification = ActorSpecification{
methods: [

View File

@@ -0,0 +1,75 @@
module generator
import freeflowuniverse.herolib.core.code { Folder, File }
import freeflowuniverse.herolib.core.texttools
// generates the folder with runnable scripts of the actor
pub fn generate_scripts_folder(name string, example bool) Folder {
actor_name := '${texttools.snake_case(name)}_actor'
return Folder {
name: 'scripts'
files: [
generate_run_script(actor_name),
generate_docs_script(actor_name),
generate_run_actor_script(name),
generate_run_actor_example_script(name),
generate_run_http_server_script(name),
// generate_compile_script(actor_name),
// generate_generate_script(actor_name)
]
}
}
// Function to generate a script for running an actor
fn generate_run_script(actor_name string) File {
actor_title := texttools.title_case(actor_name)
dollar := '$'
return File{
name: 'run'
extension:'sh'
content: $tmpl('./templates/run.sh.template')
}
}
// Function to generate a script for running an actor
fn generate_docs_script(actor_name string) File {
dollar := '$'
return File{
name: 'docs'
extension:'vsh'
content: $tmpl('./templates/docs.vsh.template')
}
}
// Function to generate a script for running an actor
fn generate_run_actor_script(name string) File {
name_snake := texttools.snake_case(name)
name_pascal := texttools.pascal_case(name)
return File{
name: 'run_actor'
extension:'vsh'
content: $tmpl('./templates/run_actor.vsh.template')
}
}
// Function to generate a script for running an example actor
fn generate_run_actor_example_script(name string) File {
name_snake := texttools.snake_case(name)
name_pascal := texttools.pascal_case(name)
return File{
name: 'run_actor_example'
extension:'vsh'
content: $tmpl('./templates/run_actor_example.vsh.template')
}
}
// Function to generate a script for running an HTTP server
fn generate_run_http_server_script(name string) File {
port := 8080
name_snake := texttools.snake_case(name)
return File{
name: 'run_http_server'
extension:'vsh'
content: $tmpl('./templates/run_http_server.vsh.template')
}
}

View File

@@ -0,0 +1,41 @@
import os
import freeflowuniverse.herolib.baobab.stage
import freeflowuniverse.herolib.core.redisclient
import freeflowuniverse.herolib.schemas.openapi
import time
pub const configuration = stage.ActorConfig {
name: '@{name_snake}'
version: '@{version}'
}
@@[heap]
struct @{name_pascal}Actor {
stage.Actor
pub mut:
@{name_snake} I@{name_pascal}
}
pub fn new(core I@{name_pascal}, config stage.ActorConfig) !&@{name_pascal}Actor {
return &@{name_pascal}Actor {
Actor: stage.new_actor(config)!
@{name_snake}: core
}
}
pub fn (mut a @{name_pascal}Actor) handle(method string, data string) !string {
action := a.act(
name: method
params: data
)!
return action.result
}
// Actor listens to the Redis queue for method invocations
pub fn (mut a @{name_pascal}Actor) run() ! {
mut rpc := a.get_redis_rpc()!
for {
rpc.process(a.handle)!
time.sleep(time.millisecond * 100) // Prevent CPU spinning
}
}

View File

@@ -0,0 +1,37 @@
import os
import freeflowuniverse.herolib.baobab.stage
import freeflowuniverse.herolib.core.redisclient
import freeflowuniverse.herolib.schemas.openapi
const name = '@{actor_name_snake}'
@@[heap]
struct @{actor_name_pascal}Actor {
stage.Actor
}
pub fn new() !&@{actor_name_pascal}Actor {
return &@{actor_name_pascal}Actor {
Actor: stage.new_actor('example_@{actor_name_snake}')!
}
}
pub fn (mut a @{actor_name_pascal}Actor) handle(method string, data string) !string {
action := a.act(
name: method
params: data
)!
return action.result
}
// Actor listens to the Redis queue for method invocations
pub fn (mut a @{actor_name_pascal}Actor) run() ! {
mut redis := redisclient.new('localhost:6379') or { panic(err) }
mut rpc := redis.rpc_get('actor_@{dollar}{a.name}')
println('Actor started and listening for tasks...')
for {
rpc.process(a.handle)!
time.sleep(time.millisecond * 100) // Prevent CPU spinning
}
}

View File

@@ -0,0 +1,10 @@
const test_port = 8101
pub fn test_new() ! {
new() or { return error('Failed to create actor:\n@{dollar}{err}') }
}
pub fn test_actor_run() ! {
mut actor := new()!
spawn actor.run()
}

View File

@@ -3,7 +3,7 @@ module @{name}
import os
import cli { Command }
import vweb
import freeflowuniverse.herolib.rpc.openrpc
import freeflowuniverse.herolib.schemas.openrpc
import freeflowuniverse.herolib.core.pathlib

View File

@@ -0,0 +1,74 @@
module pet_store_actor
import freeflowuniverse.herolib.baobab.stage
import freeflowuniverse.herolib.core.redisclient
import x.json2 as json
import time
fn mock_response() ! {
mut redis := redisclient.new('localhost:6379')!
mut rpc_q := redis.rpc_get('actor_pet_store')
for {
rpc_q.process(fn(method string, data string)!string{
return json.encode(method)
})!
time.sleep(time.millisecond * 100) // Prevent CPU spinning
}
}
fn test_list_pets() ! {
mut client := new_client()!
limit := 10
spawn mock_response()
pets := client.list_pets(limit)!
// assert pets.len <= limit
println('test_list_pets passed')
}
fn test_create_pet() ! {
mut client := new_client()!
client.create_pet()!
println('test_create_pet passed')
}
fn test_get_pet() ! {
mut client := new_client()!
pet_id := 1 // Replace with an actual pet ID in your system
pet := client.get_pet(pet_id)!
// assert pet.id == pet_id
println('test_get_pet passed')
}
fn test_delete_pet() ! {
mut client := new_client()!
pet_id := 1 // Replace with an actual pet ID in your system
client.delete_pet(pet_id)!
println('test_delete_pet passed')
}
fn test_list_orders() ! {
mut client := new_client()!
client.list_orders()!
println('test_list_orders passed')
}
fn test_get_order() ! {
mut client := new_client()!
order_id := 1 // Replace with an actual order ID in your system
order := client.get_order(order_id)!
// assert order.id == order_id
println('test_get_order passed')
}
fn test_delete_order() ! {
mut client := new_client()!
order_id := 1 // Replace with an actual order ID in your system
client.delete_order(order_id)!
println('test_delete_order passed')
}
fn test_create_user() ! {
mut client := new_client()!
client.create_user()!
println('test_create_user passed')
}

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env -S v -n -w -gc none -no-retry-compilation -cc tcc -d use_openssl -enable-globals run
import os
abs_dir_of_script := dir(@@FILE)
// Format code
println('Formatting code...')
if os.system('v fmt -w @{dollar}{abs_dir_of_script}/examples') != 0 {
eprintln('Warning: Failed to format examples')
}
if os.system('v fmt -w @{dollar}{abs_dir_of_script}/src') != 0 {
eprintln('Warning: Failed to format actor')
}
// Clean existing docs
println('Cleaning existing documentation...')
os.rmdir_all('_docs') or {}
os.rmdir_all('docs') or {}
os.rmdir_all('vdocs') or {}
herolib_path := os.join_path(abs_dir_of_script, 'lib')
os.chdir(herolib_path) or {
panic('Failed to change directory to herolib: @{dollar}{err}')
}
os.rmdir_all('_docs') or {}
os.rmdir_all('docs') or {}
os.rmdir_all('vdocs') or {}
// Generate HTML documentation
println('Generating HTML documentation...')
if os.system('v doc -m -f html . -readme -comments -no-timestamp -o ../docs') != 0 {
panic('Failed to generate HTML documentation')
}
os.chdir(abs_dir_of_script) or {
panic('Failed to change directory to abs_dir_of_script: @{dollar}{err}')
}
// Generate Markdown documentation
println('Generating Markdown documentation...')
os.rmdir_all('vdocs') or {}
if os.system('v doc -m -no-color -f md -o vdocs/herolib/') != 0 {
panic('Failed to generate Hero markdown documentation')
}
println('Documentation generation completed successfully!')

View File

@@ -0,0 +1,41 @@
import freeflowuniverse.herolib.schemas.openapi { OpenAPI }
import freeflowuniverse.herolib.baobab.stage {Client, ClientConfig}
import freeflowuniverse.herolib.schemas.openrpc { OpenRPC }
import freeflowuniverse.herolib.baobab.stage.interfaces { HTTPServer, Context }
import veb
@@[params]
pub struct HTTPServerParams {
pub:
base_url string
port int = 8080
}
pub fn new_http_server(params HTTPServerParams) !&HTTPServer {
mut s := interfaces.new_http_server()!
@if openrpc
mut openrpc_controller := new_openrpc_http_controller(HTTPServerParams{
...params,
base_url: '@{dollar}{params.base_url}/openrpc'
})!
s.register_controller[openrpc.HTTPController, Context]('/openrpc', mut openrpc_controller)!
@end
@if openapi
mut openapi_ctrl := new_openapi_http_controller(configuration, params)!
mut openapi_ex_ctrl := new_openapi_http_controller(configuration.example().example(), params)!
mut openapi_playground_controller := openapi.new_playground_controller(
base_url: '@{dollar}{params.base_url}/playground/openapi'
specification_path: openapi_spec_path
)!
s.register_controller[openapi.HTTPController, Context]('/openapi/v1', mut openapi_ctrl)!
s.register_controller[openapi.HTTPController, Context]('/openapi/example', mut openapi_ex_ctrl)!
s.register_controller[openapi.PlaygroundController, Context]('/playground/openapi', mut openapi_playground_controller)!
@end
return s
}
pub fn run_http_server(params HTTPServerParams) ! {
mut server := new_http_server(params)!
veb.run[HTTPServer, Context](mut server, params.port)
}

View File

@@ -0,0 +1,7 @@
fn test_new_http_server() ! {
new_http_server()!
}
fn test_run_http_server() ! {
spawn run_http_server()
}

View File

@@ -0,0 +1,22 @@
import freeflowuniverse.herolib.baobab.stage.interfaces
import freeflowuniverse.herolib.baobab.stage
import freeflowuniverse.herolib.schemas.openapi
pub fn new_openapi_interface(config stage.ActorConfig) !&interfaces.OpenAPIInterface {
// create OpenAPI Handler with actor's client
client := new_client(config)!
return interfaces.new_openapi_interface(client.Client)
}
@if http
// creates HTTP controller with the actor's OpenAPI Handler
// and OpenAPI Specification
pub fn new_openapi_http_controller(config stage.ActorConfig, params HTTPServerParams) !&openapi.HTTPController {
return openapi.new_http_controller(
base_url: '@{dollar}{params.base_url}/openapi/@{dollar}{config.version}'
specification: openapi_specification
specification_path: openapi_spec_path
handler: new_openapi_interface(config)!
)
}
@end

View File

@@ -0,0 +1,9 @@
fn test_new_openapi_interface() ! {
new_openapi_interface()!
}
@if http
fn test_new_openapi_http_controller() ! {
new_openapi_http_controller()!
}
@end

View File

@@ -0,0 +1,19 @@
import freeflowuniverse.herolib.baobab.stage.interfaces
import freeflowuniverse.herolib.schemas.openrpc
pub fn new_openrpc_interface() !&interfaces.OpenRPCInterface {
// create OpenRPC Handler with actor's client
client := new_client()!
return interfaces.new_openrpc_interface(client.Client)
}
@if http
// creates HTTP controller with the actor's OpenRPC Handler
// and OpenRPC Specification
pub fn new_openrpc_http_controller(params ServerParams) !&openrpc.HTTPController {
return openrpc.new_http_controller(
specification: openrpc_specification
handler: new_openrpc_interface()!
)
}
@end

View File

@@ -0,0 +1,9 @@
fn test_new_openrpc_interface() ! {
new_openrpc_interface()!
}
@if http
fn test_new_openrpc_http_controller() ! {
new_openrpc_http_controller()!
}
@end

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env -S v -n -cg -w -enable-globals run
import freeflowuniverse.herolib.baobab.actors.accountant
import freeflowuniverse.herolib.baobab.stages.accountant
import vweb
import freeflowuniverse.herolib.rpc.openrpc
import freeflowuniverse.herolib.schemas.openrpc
import os
import freeflowuniverse.herolib.core.pathlib

View File

@@ -0,0 +1,42 @@
#!/bin/bash -ex
DIR="@{dollar}(cd "@{dollar}(dirname "@{dollar}{BASH_SOURCE[0]}")" && pwd)"
echo "@{dollar}DIR"
chmod +x @{dollar}{DIR}/run_actor.vsh
@{dollar}{DIR}/run_actor.vsh &
ACTOR_PID=@{dollar}!
chmod +x @{dollar}{DIR}/run_actor_example.vsh
@{dollar}{DIR}/run_actor_example.vsh &
EXAMPLE_ACTOR_PID=@{dollar}!
chmod +x @{dollar}{DIR}/run_http_server.vsh
@{dollar}{DIR}/run_http_server.vsh &
HTTP_SERVER_PID=@{dollar}!
# Print desired output
echo "${actor_title} Actor Redis Interface running on redis://localhost:6379"
echo "* /queues/${actor_name} -> Action Interface"
echo ""
echo "${actor_title} Actor HTTP Server running on http://localhost:8080"
echo "* http://localhost:8080/playground/openapi -> OpenAPI Playground"
echo "* http://localhost:8080/openapi -> OpenAPI Interface"
# echo "* http://localhost:8080/docs -> Documentation"
echo ""
# Function to clean up when script is killed
cleanup() {
echo "Stopping background processes..."
kill "@{dollar}ACTOR_PID" "@{dollar}HTTP_SERVER_PID" 2>/dev/null
wait
echo "All processes stopped."
exit 0
}
# Trap SIGINT (Ctrl+C), SIGTERM, and SIGQUIT to call cleanup
trap cleanup SIGINT SIGTERM SIGQUIT
# Wait for processes to finish
wait

View File

@@ -0,0 +1,10 @@
#!/usr/bin/env -S v -w -n -enable-globals run
import @{name_snake}
mut actor := @{name_snake}.new(
@{name_snake}.new_@{name_snake}()!,
@{name_snake}.configuration
)!
actor.run()!

View File

@@ -0,0 +1,10 @@
#!/usr/bin/env -S v -w -n -enable-globals run
import @{name_snake}
mut actor := @{name_snake}.new(
@{name_snake}.new_@{name_snake}_example()!,
@{name_snake}.configuration.example()
)!
actor.run()!

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env -S v -w -n -enable-globals run
import @{name_snake}
@{name_snake}.run_http_server(
base_url: 'http://localhost:@{port}'
port: @{port}
)!

View File

@@ -0,0 +1,14 @@
import freeflowuniverse.herolib.schemas.openapi
import freeflowuniverse.herolib.schemas.openrpc
import os
@if support_openrpc
const openrpc_spec_path = '@{dollar}{os.dir(@@FILE)}/docs/specs/openrpc.json'
const openrpc_spec_json = os.read_file(openrpc_spec_path) or { panic(err) }
const openrpc_specification = openrpc.decode(openrpc_spec_json)!
@end
@if support_openapi
const openapi_spec_path = '@{dollar}{os.dir(os.dir(@@FILE))}/docs/specs/openapi.json'
const openapi_spec_json = os.read_file(openapi_spec_path) or { panic(err) }
const openapi_specification = openapi.json_decode(openapi_spec_json)!
@end

View File

@@ -0,0 +1 @@
pet_store_actor

View File

@@ -1,9 +1,7 @@
module generator
import freeflowuniverse.herolib.core.code
import freeflowuniverse.herolib.core.codeparser
import os
import freeflowuniverse.herolib.ui.console
// // generate_object_methods generates CRUD actor methods for a provided structure
// pub fn (generator ActorGenerator) generate_object_methods(structure code.Struct) []code.Function {
@@ -19,11 +17,11 @@ import freeflowuniverse.herolib.ui.console
pub fn test_generate_get_method() {
generator := ActorGenerator{'test'}
actor_struct := code.Struct{
name: 'TestActor'
name: 'TestActor'
fields: [
code.StructField{
name: 'test_struct_map'
typ: code.Type{
typ: code.Type{
symbol: 'map[string]&TestStruct'
}
},
@@ -43,7 +41,6 @@ pub fn test_generate_get_method() {
actor_field: field
root_struct: test_struct
)
// console.print_debug(method.vgen())
}
// // generate_object_methods generates CRUD actor methods for a provided structure

View File

@@ -0,0 +1,8 @@
module osis
pub fn new(config OSISConfig) !OSIS {
return OSIS{
indexer: new_indexer()!
storer: new_storer()!
}
}

116
lib/baobab/osis/indexer.v Normal file
View File

@@ -0,0 +1,116 @@
module osis
import json
import db.sqlite
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.core.pathlib
pub struct Indexer {
db sqlite.DB
}
@[params]
pub struct IndexerConfig {
db_path string
reset bool
}
pub fn new_indexer(config IndexerConfig) !Indexer {
return Indexer{}
}
// deletes an indexer table belonging to a base object
pub fn reset(path string) ! {
mut db_file := pathlib.get_file(path: path)!
db_file.delete()!
}
pub fn (mut i Indexer) new_generic[T](id u32, object T) !u32 {
return i.new(get_table[T](), id, get_indices[T](object))!
}
// new creates a new root object entry in the root_objects table,
// and the table belonging to the type of root object with columns for index fields
pub fn (mut i Indexer) new(table string, id u32, indices map[string]string) !u32 {
insert_query := 'INSERT into ${table} (${indices.keys().join(',')}) values (${indices.values().join(',')})'
i.db.exec(insert_query) or {
return error('Error inserting object ${id} into table ${table}\n${err}')
}
return 0
}
// save the session to redis & mem
pub fn (mut backend Indexer) set(obj RootObject) ! {
panic('implement')
}
// save the session to redis & mem
pub fn (mut backend Indexer) delete(id string, obj RootObject) ! {
panic('implement')
}
pub fn (mut backend Indexer) get(id string, obj RootObject) !RootObject {
panic('implement')
}
pub fn (mut backend Indexer) get_json(id string, obj RootObject) !string {
panic('implement')
}
pub fn (mut backend Indexer) list(obj RootObject) ![]u32 {
panic('implement')
}
// from and to for int f64 time etc.
@[params]
pub struct FilterParams {
// indices map[string]string // map of index values that are being filtered by, in order of priority.
limit int // limit to the number of values to be returned, in order of priority
fuzzy bool // if fuzzy matching is enabled in matching indices
matches_all bool // if results should match all indices or any
}
// filter lists root objects of type T that match provided index parameters and params.
pub fn (mut backend Indexer) filter(filter RootObject, params FilterParams) ![]string {
panic('implement')
}
// create_root_struct_table creates a table for a root_struct with columns for each index field
fn (mut backend Indexer) create_root_object_table(object RootObject) ! {
panic('implement')
}
// deletes an indexer table belonging to a root object
fn (mut backend Indexer) delete_table(object RootObject)! {
panic('implement')
}
fn (mut backend Indexer) get_table_indices(table_name string) ![]string {
panic('implement')
}
fn (mut backend Indexer) table_exists(table_name string) !bool {
panic('implement')
}
// get_table_name returns the name of the table belonging to a root struct
fn get_table_name(object RootObject) string {
panic('implement')
}
// get_table_name returns the name of the table belonging to a root struct
fn get_table[T]() string {
return typeof[T]()
}
// returns the lists of the indices of a root objects db table, and corresponding values
pub fn get_indices[T](object T) map[string]string {
mut indices := map[string]string
$for field in T.fields {
if field.attrs.contains('index') {
value := object.$(field.name)
indices[field.name] = '${value}'
}
}
return indices
}

16
lib/baobab/osis/model.v Normal file
View File

@@ -0,0 +1,16 @@
module osis
pub struct OSIS {
pub mut:
indexer Indexer // storing indeces
storer Storer
}
@[params]
pub struct OSISConfig {
pub:
directory string
name string
secret string
reset bool
}

View File

@@ -1,10 +1,6 @@
module osis
import os
import db.sqlite
import db.pg
import freeflowuniverse.herolib.data.dbfs
import freeflowuniverse.herolib.data.encoderhero
pub fn (mut o OSIS) generic_new[T](obj T) !u32 {
id := o.indexer.generic_new[T](obj)!
@@ -12,10 +8,20 @@ pub fn (mut o OSIS) generic_new[T](obj T) !u32 {
return id
}
pub fn (mut o OSIS) new[T](obj T) !u32 {
id := o.storer.new_generic[T](obj)!
o.indexer.new_generic[T](id, obj)!
return id
}
pub fn (mut o OSIS) generic_get[T](id u32) !T {
return o.storer.generic_get[T](id)!
}
pub fn (mut o OSIS) get[T](id u32) !T {
return o.storer.generic_get[T](u32(id))!
}
pub fn (mut o OSIS) generic_set[T](obj T) ! {
o.indexer.generic_set[T](obj) or { return error('Failed to set new indices:\n${err}') }
o.storer.generic_set[T](obj)!
@@ -26,6 +32,16 @@ pub fn (mut o OSIS) generic_delete[T](id u32) ! {
o.storer.generic_delete[T](id)!
}
pub fn (mut o OSIS) delete(id u32) ! {
o.storer.delete(u32(id))!
}
pub fn (mut o OSIS) list[T]() ![]T {
panic('implement')
// ids := o.indexer.generic_list[T]()!
// return o.storer.generic_list[T](ids)!
}
pub fn (mut o OSIS) generic_list[T]() ![]T {
ids := o.indexer.generic_list[T]()!
return o.storer.generic_list[T](ids)!

15
lib/baobab/osis/storer.v Normal file
View File

@@ -0,0 +1,15 @@
module osis
import freeflowuniverse.herolib.data.ourdb {OurDB}
import os
pub struct Storer {
pub mut:
db OurDB
}
pub fn new_storer() !Storer {
return Storer {
db: ourdb.new()!
}
}

View File

@@ -0,0 +1,23 @@
module osis
import json
// new creates a new root object entry in the root_objects table,
// and the table belonging to the type of root object with columns for index fields
pub fn (mut storer Storer) new_generic[T](obj T) !u32 {
data := json.encode(obj).bytes()
return storer.db.set(data: data)
}
pub fn (mut storer Storer) generic_get[T](id u32) !T {
return json.decode(T, storer.db.get(id)!.bytestr())
}
pub fn (mut storer Storer) generic_set[T](obj T) ! {
data := json.encode(obj).bytes()
return storer.db.set(data: data)
}
pub fn (mut storer Storer) delete(id u32) ! {
storer.db.delete(id)!
}

View File

@@ -0,0 +1,168 @@
module specification
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.core.code { Struct, Function }
import freeflowuniverse.herolib.schemas.jsonschema { Schema, SchemaRef }
import freeflowuniverse.herolib.schemas.openapi { Operation, Parameter, OpenAPI, Components, Info, PathItem, ServerSpec, MediaType }
import freeflowuniverse.herolib.schemas.openrpc { ExamplePairing, Example, ExampleRef, ContentDescriptor, ErrorSpec }
// Helper function: Convert OpenAPI parameter to ContentDescriptor
fn openapi_param_to_content_descriptor(param Parameter) ContentDescriptor {
return ContentDescriptor{
name: param.name,
summary: param.description,
description: param.description,
required: param.required,
schema: param.schema
}
}
// Helper function: Convert OpenAPI parameter to ContentDescriptor
fn openapi_param_to_example(param Parameter) ?Example {
if param.schema is Schema {
if param.schema.example.str() != '' {
return Example{
name: 'Example ${param.name}',
description: 'Example ${param.description}',
value: param.schema.example
}
}
}
return none
}
// Helper function: Convert OpenAPI operation to ActorMethod
fn openapi_operation_to_actor_method(info openapi.OperationInfo) ActorMethod {
mut parameters := []ContentDescriptor{}
mut example_parameters:= []Example{}
for param in info.operation.parameters {
parameters << openapi_param_to_content_descriptor(param)
example_parameters << openapi_param_to_example(param) or {
continue
}
}
if schema_ := info.operation.payload_schema() {
// TODO: document assumption
schema := Schema{...schema_, title: texttools.pascal_case(info.operation.operation_id)}
parameters << ContentDescriptor {name: 'data', schema: SchemaRef(schema)}
}
mut success_responses := map[string]MediaType{}
for code, response in info.operation.responses {
if code.starts_with('2') { // Matches all 2xx responses
success_responses[code] = response.content['application/json']
}
}
if success_responses.len > 1 || success_responses.len == 0 {
panic('Actor specification must specify one successful response.')
}
response_success := success_responses.values()[0]
mut result := ContentDescriptor{
name: "result",
description: "The response of the operation.",
required: true,
schema: response_success.schema
}
example_result := if response_success.example.str() != '' {
Example{
name: 'Example response',
value: response_success.example
}
} else {Example{}}
pairing := if example_result != Example{} || example_parameters.len > 0 {
ExamplePairing{
params: example_parameters.map(ExampleRef(it))
result: ExampleRef(example_result)
}
} else {ExamplePairing{}}
mut errors := []ErrorSpec{}
for status, response in info.operation.responses {
if status.int() >= 400 {
error_schema := if response.content.len > 0 {
response.content.values()[0].schema
} else {Schema{}}
errors << ErrorSpec{
code: status.int(),
message: response.description,
data: error_schema, // Extend if error schema is defined
}
}
}
return ActorMethod{
name: info.operation.operation_id,
description: info.operation.description,
summary: info.operation.summary,
parameters: parameters,
example: pairing
result: result,
errors: errors,
}
}
// Helper function: Convert OpenAPI schema to Struct
fn openapi_schema_to_struct(name string, schema SchemaRef) Struct {
// Assuming schema properties can be mapped to Struct fields
return Struct{
name: name,
}
}
// Converts OpenAPI to ActorSpecification
pub fn from_openapi(spec_raw OpenAPI) !ActorSpecification {
spec := openapi.process(spec_raw)!
mut objects := []BaseObject{}
// get all operations for path as list of tuple [](path_string, http.Method, openapi.Operation)
// Extract methods from OpenAPI paths
// for path, item in spec.paths {
// if item.get.operation_id != '' {
// methods << openapi_operation_to_actor_method(item.get, item.get.operation_id, path)
// }
// if item.post.operation_id != '' {
// methods << openapi_operation_to_actor_method(item.post, item.post.operation_id, path)
// }
// if item.put.operation_id != '' {
// methods << openapi_operation_to_actor_method(item.put, item.put.operation_id, path)
// }
// if item.delete.operation_id != '' {
// methods << openapi_operation_to_actor_method(item.delete, item.delete.operation_id, path)
// }
// if item.patch.operation_id != '' {
// methods << openapi_operation_to_actor_method(item.patch, item.patch.operation_id, path)
// }
// if item.head.operation_id != '' {
// methods << openapi_operation_to_actor_method(item.head, item.head.operation_id, path)
// }
// if item.options.operation_id != '' {
// methods << openapi_operation_to_actor_method(item.options, item.options.operation_id, path)
// }
// if item.trace.operation_id != '' {
// methods << openapi_operation_to_actor_method(item.trace, item.trace.operation_id, path)
// }
// }
// Extract objects from OpenAPI components.schemas
for name, schema in spec.components.schemas {
objects << BaseObject{schema: schema as Schema}
}
return ActorSpecification{
openapi: spec_raw
name: spec.info.title,
description: spec.info.description,
structure: Struct{}, // Assuming no top-level structure for this use case
interfaces: [.openapi], // Default to OpenAPI for input
methods: spec.get_operations().map(openapi_operation_to_actor_method(it)),
objects: objects,
}
}

View File

@@ -0,0 +1,400 @@
module specification
import x.json2 as json {Any}
import freeflowuniverse.herolib.core.code { Struct, Function }
import freeflowuniverse.herolib.schemas.openrpc { ContentDescriptor, ErrorSpec }
import freeflowuniverse.herolib.schemas.openapi { OpenAPI, Info, ServerSpec, Components, Operation, PathItem, PathRef }
import freeflowuniverse.herolib.schemas.jsonschema {Schema, Reference, SchemaRef}
const openapi_spec = openapi.OpenAPI{
openapi: '3.0.3'
info: openapi.Info{
title: 'Pet Store API'
description: 'A sample API for a pet store'
version: '1.0.0'
}
servers: [
openapi.ServerSpec{
url: 'https://api.petstore.example.com/v1'
description: 'Production server'
},
openapi.ServerSpec{
url: 'https://staging.petstore.example.com/v1'
description: 'Staging server'
}
]
paths: {
'/pets': openapi.PathItem{
get: openapi.Operation{
summary: 'List all pets'
operation_id: 'listPets'
parameters: [
openapi.Parameter{
name: 'limit'
in_: 'query'
description: 'Maximum number of pets to return'
required: false
schema: Schema{
typ: 'integer'
format: 'int32'
example: 10
}
}
]
responses: {
'200': openapi.ResponseSpec{
description: 'A paginated list of pets'
content: {
'application/json': openapi.MediaType{
schema: Reference{
ref: '#/components/schemas/Pets'
}
example: json.raw_decode('[
{ "id": 1, "name": "Fluffy", "tag": "dog" },
{ "id": 2, "name": "Whiskers", "tag": "cat" }
]')!
}
}
}
'400': openapi.ResponseSpec{
description: 'Invalid request'
}
}
}
post: openapi.Operation{
summary: 'Create a new pet'
operation_id: 'createPet'
request_body: openapi.RequestBody{
required: true
content: {
'application/json': openapi.MediaType{
schema: Reference{
ref: '#/components/schemas/NewPet'
}
example: json.raw_decode('{ "name": "Bella", "tag": "dog" }')!
}
}
}
responses: {
'201': openapi.ResponseSpec{
description: 'Pet created'
content: {
'application/json': openapi.MediaType{
schema: Reference{
ref: '#/components/schemas/Pet'
}
example: json.raw_decode('{ "id": 3, "name": "Bella", "tag": "dog" }')!
}
}
}
'400': openapi.ResponseSpec{
description: 'Invalid input'
}
}
}
}
'/pets/{petId}': openapi.PathItem{
get: openapi.Operation{
summary: 'Get a pet by ID'
operation_id: 'getPet'
parameters: [
openapi.Parameter{
name: 'petId'
in_: 'path'
description: 'ID of the pet to retrieve'
required: true
schema: Schema{
typ: 'integer'
format: 'int64'
example: 1
}
}
]
responses: {
'200': openapi.ResponseSpec{
description: 'A pet'
content: {
'application/json': openapi.MediaType{
schema: Reference{
ref: '#/components/schemas/Pet'
}
example: json.raw_decode('{ "id": 1, "name": "Fluffy", "tag": "dog" }')!
}
}
}
'404': openapi.ResponseSpec{
description: 'Pet not found'
}
}
}
delete: openapi.Operation{
summary: 'Delete a pet by ID'
operation_id: 'deletePet'
parameters: [
openapi.Parameter{
name: 'petId'
in_: 'path'
description: 'ID of the pet to delete'
required: true
schema: Schema{
typ: 'integer'
format: 'int64'
example: 1
}
}
]
responses: {
'204': openapi.ResponseSpec{
description: 'Pet deleted'
}
'404': openapi.ResponseSpec{
description: 'Pet not found'
}
}
}
}
}
components: openapi.Components{
schemas: {
'Pet': SchemaRef(Schema{
typ: 'object'
required: ['id', 'name']
properties: {
'id': SchemaRef(Schema{
typ: 'integer'
format: 'int64'
})
'name': SchemaRef(Schema{
typ: 'string'
})
'tag': SchemaRef(Schema{
typ: 'string'
})
}
})
'NewPet': SchemaRef(Schema{
typ: 'object'
required: ['name']
properties: {
'name': SchemaRef(Schema{
typ: 'string'
})
'tag': SchemaRef(Schema{
typ: 'string'
})
}
})
'Pets': SchemaRef(Schema{
typ: 'array'
items: SchemaRef(Reference{
ref: '#/components/schemas/Pet'
})
})
}
}
}
const actor_spec = specification.ActorSpecification{
name: 'Pet Store API'
description: 'A sample API for a pet store'
structure: code.Struct{}
interfaces: [.openapi]
methods: [
specification.ActorMethod{
name: 'listPets'
summary: 'List all pets'
example: openrpc.ExamplePairing{
params: [
openrpc.ExampleRef(openrpc.Example{
name: 'Example limit'
description: 'Example Maximum number of pets to return'
value: 10
})
]
result: openrpc.ExampleRef(openrpc.Example{
name: 'Example response'
value: json.raw_decode('[
{"id": 1, "name": "Fluffy", "tag": "dog"},
{"id": 2, "name": "Whiskers", "tag": "cat"}
]')!
})
}
parameters: [
openrpc.ContentDescriptor{
name: 'limit'
summary: 'Maximum number of pets to return'
description: 'Maximum number of pets to return'
required: false
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'integer'
format: 'int32'
example: 10
})
}
]
result: openrpc.ContentDescriptor{
name: 'result'
description: 'The response of the operation.'
required: true
schema: jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/Pets'
})
}
errors: [
openrpc.ErrorSpec{
code: 400
message: 'Invalid request'
}
]
},
specification.ActorMethod{
name: 'createPet'
summary: 'Create a new pet'
example: openrpc.ExamplePairing{
result: openrpc.ExampleRef(openrpc.Example{
name: 'Example response'
value: '[]'
})
}
result: openrpc.ContentDescriptor{
name: 'result'
description: 'The response of the operation.'
required: true
}
errors: [
openrpc.ErrorSpec{
code: 400
message: 'Invalid input'
}
]
},
specification.ActorMethod{
name: 'getPet'
summary: 'Get a pet by ID'
example: openrpc.ExamplePairing{
params: [
openrpc.ExampleRef(openrpc.Example{
name: 'Example petId'
description: 'Example ID of the pet to retrieve'
value: 1
})
]
result: openrpc.ExampleRef(openrpc.Example{
name: 'Example response'
value: json.raw_decode('{"id": 1, "name": "Fluffy", "tag": "dog"}')!
})
}
parameters: [
openrpc.ContentDescriptor{
name: 'petId'
summary: 'ID of the pet to retrieve'
description: 'ID of the pet to retrieve'
required: true
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'integer'
format: 'int64'
example: 1
})
}
]
result: openrpc.ContentDescriptor{
name: 'result'
description: 'The response of the operation.'
required: true
schema: jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/Pet'
})
}
errors: [
openrpc.ErrorSpec{
code: 404
message: 'Pet not found'
}
]
},
specification.ActorMethod{
name: 'deletePet'
summary: 'Delete a pet by ID'
example: openrpc.ExamplePairing{
params: [
openrpc.ExampleRef(openrpc.Example{
name: 'Example petId'
description: 'Example ID of the pet to delete'
value: 1
})
]
}
parameters: [
openrpc.ContentDescriptor{
name: 'petId'
summary: 'ID of the pet to delete'
description: 'ID of the pet to delete'
required: true
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'integer'
format: 'int64'
example: 1
})
}
]
result: openrpc.ContentDescriptor{
name: 'result'
description: 'The response of the operation.'
required: true
}
errors: [
openrpc.ErrorSpec{
code: 404
message: 'Pet not found'
}
]
}
]
objects: [
specification.BaseObject{
schema: jsonschema.Schema{
typ: 'object'
properties: {
'id': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'integer'
format: 'int64'
}),
'name': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
}),
'tag': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}
required: ['id', 'name']
}
},
specification.BaseObject{
schema: jsonschema.Schema{
typ: 'object'
properties: {
'name': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
}),
'tag': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}
required: ['name']
}
},
specification.BaseObject{
schema: jsonschema.Schema{
typ: 'array'
items: jsonschema.Items(jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/Pet'
}))
}
}
]
}
pub fn test_from_openapi() ! {
// panic(from_openapi(openapi_spec)!)
assert from_openapi(openapi_spec)! == actor_spec
}

View File

@@ -0,0 +1,105 @@
module specification
import freeflowuniverse.herolib.schemas.openrpc { OpenRPC, Method, ContentDescriptor, ErrorSpec }
import freeflowuniverse.herolib.schemas.jsonschema { Reference, Schema, SchemaRef }
import freeflowuniverse.herolib.core.texttools
// Helper function: Convert OpenRPC Method to ActorMethod
fn openrpc_method_to_actor_method(method Method) ActorMethod {
mut parameters := []ContentDescriptor{}
mut errors := []ErrorSpec{}
// Process parameters
for param in method.params {
if param is ContentDescriptor {
parameters << param
} else {
panic('Method param should be inflated')
}
}
// Process errors
for err in method.errors {
if err is ErrorSpec {
errors << err
} else {
panic('Method error should be inflated')
}
}
if method.result is Reference {
panic('Method result should be inflated')
}
return ActorMethod{
name: method.name
description: method.description
summary: method.summary
parameters: parameters
result: method.result as ContentDescriptor
errors: errors
}
}
// // Helper function: Extract Structs from OpenRPC Components
// fn extract_structs_from_openrpc(openrpc OpenRPC) []Struct {
// mut structs := []Struct{}
// for schema_name, schema in openrpc.components.schemas {
// if schema is Schema {
// mut fields := []Struct.Field{}
// for field_name, field_schema in schema.properties {
// if field_schema is Schema {
// fields << Struct.Field{
// name: field_name
// typ: field_schema.to_code() or { panic(err) }
// description: field_schema.description
// required: field_name in schema.required
// }
// }
// }
// structs << Struct{
// name: schema_name
// description: schema.description
// fields: fields
// }
// }
// }
// return structs
// }
// Converts OpenRPC to ActorSpecification
pub fn from_openrpc(spec OpenRPC) !ActorSpecification {
mut methods := []ActorMethod{}
mut objects := []BaseObject{}
// Process methods
for method in spec.methods {
methods << openrpc_method_to_actor_method(spec.inflate_method(method))
}
// Process objects (schemas)
// structs := extract_structs_from_openrpc(spec)
for key, schema in spec.components.schemas {
if schema is Schema {
if schema.typ == 'object' {
objects << BaseObject{
schema: Schema {...schema,
title: texttools.name_fix_pascal(key)
id: texttools.snake_case(key)
}
}
}
}
}
return ActorSpecification{
name: spec.info.title
description: spec.info.description
interfaces: [.openrpc]
methods: methods
objects: objects
}
}

View File

@@ -0,0 +1,393 @@
module specification
import freeflowuniverse.herolib.core.code { Struct, Function }
import freeflowuniverse.herolib.schemas.openrpc { ContentDescriptor, ErrorSpec }
import freeflowuniverse.herolib.schemas.openapi { OpenAPI, Info, ServerSpec, Components, Operation, PathItem, PathRef }
import freeflowuniverse.herolib.schemas.jsonschema {Schema, Reference, SchemaRef}
const openrpc_spec = openrpc.OpenRPC{
openrpc: '1.0.0-rc1'
info: openrpc.Info{
title: 'Petstore'
license: openrpc.License{
name: 'MIT'
}
version: '1.0.0'
}
servers: [openrpc.Server{
name: 'localhost'
url: openrpc.RuntimeExpression('http://localhost:8080')
}]
methods: [
openrpc.Method{
name: 'list_pets'
summary: 'List all pets'
params: [openrpc.ContentDescriptorRef(openrpc.ContentDescriptor{
name: 'limit'
description: 'How many items to return at one time (max 100)'
required: false
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'integer'
minimum: 1
})
})]
result: openrpc.ContentDescriptorRef(openrpc.ContentDescriptor{
name: 'pets'
description: 'A paged array of pets'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'array'
items: jsonschema.Items(jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/Pet'
}))
})
})
examples: [openrpc.ExamplePairing{
name: 'listPetExample'
description: 'List pet example'
}]
},
openrpc.Method{
name: 'create_pet'
summary: 'Create a pet'
params: [
openrpc.ContentDescriptorRef(openrpc.ContentDescriptor{
name: 'newPetName'
description: 'Name of pet to create'
required: true
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}),
openrpc.ContentDescriptorRef(openrpc.ContentDescriptor{
name: 'newPetTag'
description: 'Pet tag to create'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
})
]
result: openrpc.ContentDescriptorRef(jsonschema.Reference{
ref: '#/components/contentDescriptors/PetId'
})
examples: [openrpc.ExamplePairing{
name: 'createPetExample'
description: 'Create pet example'
}]
},
openrpc.Method{
name: 'get_pet'
summary: 'Info for a specific pet'
params: [openrpc.ContentDescriptorRef(jsonschema.Reference{
ref: '#/components/contentDescriptors/PetId'
})]
result: openrpc.ContentDescriptorRef(openrpc.ContentDescriptor{
name: 'pet'
description: 'Expected response to a valid request'
schema: jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/Pet'
})
})
examples: [openrpc.ExamplePairing{
name: 'getPetExample'
description: 'Get pet example'
}]
},
openrpc.Method{
name: 'update_pet'
summary: 'Update a pet'
params: [
openrpc.ContentDescriptorRef(jsonschema.Reference{
ref: '#/components/contentDescriptors/PetId'
}),
openrpc.ContentDescriptorRef(openrpc.ContentDescriptor{
name: 'updatedPetName'
description: 'New name for the pet'
required: true
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}),
openrpc.ContentDescriptorRef(openrpc.ContentDescriptor{
name: 'updatedPetTag'
description: 'New tag for the pet'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
})
]
result: openrpc.ContentDescriptorRef(openrpc.ContentDescriptor{
name: 'pet'
description: 'Updated pet object'
schema: jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/Pet'
})
})
examples: [openrpc.ExamplePairing{
name: 'updatePetExample'
description: 'Update pet example'
}]
},
openrpc.Method{
name: 'delete_pet'
summary: 'Delete a pet'
params: [openrpc.ContentDescriptorRef(jsonschema.Reference{
ref: '#/components/contentDescriptors/PetId'
})]
result: openrpc.ContentDescriptorRef(openrpc.ContentDescriptor{
name: 'success'
description: 'Boolean indicating success'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'boolean'
})
})
examples: [openrpc.ExamplePairing{
name: 'deletePetExample'
description: 'Delete pet example'
}]
}
]
components: openrpc.Components{
content_descriptors: {
'PetId': openrpc.ContentDescriptorRef(openrpc.ContentDescriptor{
name: 'petId'
description: 'The ID of the pet'
required: true
schema: jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/PetId'
})
})
}
schemas: {
'PetId': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'integer'
minimum: 0
}),
'Pet': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'object'
properties: {
'id': jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/PetId'
}),
'name': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
}),
'tag': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}
required: ['id', 'name']
})
}
}
}
const actor_spec = specification.ActorSpecification{
name: 'Petstore'
structure: code.Struct{
is_pub: false
}
interfaces: [.openrpc]
methods: [specification.ActorMethod{
name: 'list_pets'
summary: 'List all pets'
parameters: [openrpc.ContentDescriptor{
name: 'limit'
description: 'How many items to return at one time (max 100)'
required: false
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'integer'
minimum: 1
})
}]
result: openrpc.ContentDescriptor{
name: 'pets'
description: 'A paged array of pets'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'array'
items: jsonschema.Items(jsonschema.SchemaRef(jsonschema.Schema{
typ: 'object'
properties: {
'id': jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/PetId'
}),
'name': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
}),
'tag': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}
required: ['id', 'name']
}))
})
}
}, specification.ActorMethod{
name: 'create_pet'
summary: 'Create a pet'
parameters: [openrpc.ContentDescriptor{
name: 'newPetName'
description: 'Name of pet to create'
required: true
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}, openrpc.ContentDescriptor{
name: 'newPetTag'
description: 'Pet tag to create'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}]
}, specification.ActorMethod{
name: 'get_pet'
summary: 'Info for a specific pet'
result: openrpc.ContentDescriptor{
name: 'pet'
description: 'Expected response to a valid request'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'object'
properties: {
'id': jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/PetId'
}),
'name': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
}),
'tag': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}
required: ['id', 'name']
})
}
}, specification.ActorMethod{
name: 'update_pet'
summary: 'Update a pet'
parameters: [openrpc.ContentDescriptor{
name: 'updatedPetName'
description: 'New name for the pet'
required: true
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}, openrpc.ContentDescriptor{
name: 'updatedPetTag'
description: 'New tag for the pet'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}]
result: openrpc.ContentDescriptor{
name: 'pet'
description: 'Updated pet object'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'object'
properties: {
'id': jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/PetId'
}),
'name': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
}),
'tag': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}
required: ['id', 'name']
})
}
}, specification.ActorMethod{
name: 'delete_pet'
summary: 'Delete a pet'
result: openrpc.ContentDescriptor{
name: 'success'
description: 'Boolean indicating success'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'boolean'
})
}
}]
objects: [specification.BaseObject{
schema: jsonschema.Schema{
id: 'pet'
title: 'Pet'
typ: 'object'
properties: {
'id': jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/PetId'
}),
'name': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
}),
'tag': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}
required: ['id', 'name']
}
}]
}
pub fn test_from_openrpc() ! {
actor_spec_ := from_openrpc(openrpc_spec)!
assert actor_spec_.methods.len == actor_spec.methods.len
assert_methods_match(actor_spec_.methods[0], actor_spec.methods[0])
// assert from_openrpc(openrpc_spec)! == actor_spec
}
fn assert_methods_match(a ActorMethod, b ActorMethod) {
// Compare method names
assert a.name == b.name, 'Method names do not match: ${a.name} != ${b.name}'
// Compare summaries
assert a.summary == b.summary, 'Method summaries do not match for method ${a.name}.'
// Compare descriptions
assert a.description == b.description, 'Method descriptions do not match for method ${a.name}.'
// Compare parameters count
assert a.parameters.len == b.parameters.len, 'Parameter counts do not match for method ${a.name}.'
// Compare each parameter
for i, param_a in a.parameters {
assert_params_match(param_a, b.parameters[i], a.name)
}
// Compare result
assert_params_match(a.result, b.result, a.name)
}
fn assert_params_match(a openrpc.ContentDescriptor, b openrpc.ContentDescriptor, method_name string) {
// Compare parameter names
assert a.name == b.name, 'Parameter names do not match in method ${method_name}: ${a.name} != ${b.name}'
// Compare summaries
assert a.summary == b.summary, 'Parameter summaries do not match in method ${method_name}: ${a.name}'
// Compare descriptions
assert a.description == b.description, 'Parameter descriptions do not match in method ${method_name}: ${a.name}'
// Compare required flags
assert a.required == b.required, 'Required flags do not match in method ${method_name}: ${a.name}'
// Compare schemas
// assert_schemas_match(a.schema, b.schema, method_name, a.name)
}
// fn assert_schemas_match(a jsonschema.SchemaRef, b jsonschema.SchemaRef, method_name string, param_name string) {
// if a is Schema &&
// // Compare schema types
// assert a.typ == b.typ, 'Schema types do not match for parameter ${param_name} in method ${method_name}: ${a.typ} != ${b.typ}'
// // Compare schema titles
// assert a.title == b.title, 'Schema titles do not match for parameter ${param_name} in method ${method_name}.'
// // Compare schema descriptions
// assert a.description == b.description, 'Schema descriptions do not match for parameter ${param_name} in method ${method_name}.'
// // Compare other schema fields as needed (e.g., properties, additional properties, items, etc.)
// // Add more checks here if needed for deeper schema comparisons
// }

View File

@@ -0,0 +1,202 @@
module specification
import freeflowuniverse.herolib.core.code { Struct, Function }
import freeflowuniverse.herolib.schemas.openapi
import freeflowuniverse.herolib.schemas.openrpc {ExamplePairing, ContentDescriptor, ErrorSpec}
import freeflowuniverse.herolib.schemas.jsonschema {Schema, Reference}
pub struct ActorSpecification {
pub mut:
version string = '1.0.0'
openapi ?openapi.OpenAPI
openrpc ?openrpc.OpenRPC
name string @[omitempty]
description string @[omitempty]
structure Struct @[omitempty]
interfaces []ActorInterface @[omitempty]
methods []ActorMethod @[omitempty]
objects []BaseObject @[omitempty]
}
pub enum ActorInterface {
openrpc
openapi
webui
command
http
}
pub struct ActorMethod {
pub:
name string @[omitempty]
description string @[omitempty]
summary string
example ExamplePairing
parameters []ContentDescriptor
result ContentDescriptor
errors []ErrorSpec
category MethodCategory
}
pub struct BaseObject {
pub mut:
schema Schema
new_method ?ActorMethod
get_method ?ActorMethod
set_method ?ActorMethod
delete_method ?ActorMethod
list_method ?ActorMethod
filter_method ?ActorMethod
other_methods []ActorMethod
}
pub enum MethodCategory {
base_object_new
base_object_get
base_object_set
base_object_delete
base_object_list
other
}
// returns whether method belongs to a given base object
// TODO: link to more info about base object methods
fn (m ActorMethod) belongs_to_object(obj BaseObject) bool {
base_obj_is_param := m.parameters
.filter(it.schema is Schema)
.map(it.schema as Schema)
.any(it.id == obj.schema.id)
base_obj_is_result := if m.result.schema is Schema {
m.result.schema.id == obj.schema.id
} else {
ref := m.result.schema as Reference
ref.ref.all_after_last('/') == obj.name()
}
return base_obj_is_param || base_obj_is_result
}
pub fn (s ActorSpecification) validate() ActorSpecification {
mut validated_objects := []BaseObject{}
for obj_ in s.objects {
mut obj := obj_
if obj.schema.id == '' {
obj.schema.id = obj.schema.title
}
methods := s.methods.filter(it.belongs_to_object(obj))
if m := methods.filter(it.is_new_method())[0] {
obj.new_method = m
}
if m := methods.filter(it.is_set_method())[0] {
obj.set_method = m
}
if m := methods.filter(it.is_get_method())[0] {
obj.get_method = m
}
if m := methods.filter(it.is_delete_method())[0] {
obj.delete_method = m
}
if m := methods.filter(it.is_list_method())[0] {
obj.list_method = m
}
validated_objects << BaseObject {
...obj
other_methods: methods.filter(!it.is_crudlf_method())
}
}
return ActorSpecification {
...s,
objects: validated_objects
}
}
// method category returns what category a method falls under
pub fn (s ActorSpecification) method_type(method ActorMethod) MethodCategory {
return if s.is_base_object_new_method(method) {
.base_object_new
} else if s.is_base_object_get_method(method) {
.base_object_get
} else if s.is_base_object_set_method(method) {
.base_object_set
} else if s.is_base_object_delete_method(method) {
.base_object_delete
} else if s.is_base_object_list_method(method) {
.base_object_list
} else {
.other
}
}
// a base object method is a method that is a
// CRUD+list+filter method of a base object
fn (s ActorSpecification) is_base_object_method(method ActorMethod) bool {
base_obj_is_param := method.parameters
.filter(it.schema is Schema)
.map(it.schema as Schema)
.any(it.id in s.objects.map(it.schema.id))
base_obj_is_result := if method.result.schema is Schema {
method.result.schema.id in s.objects.map(it.name())
} else {
ref := method.result.schema as Reference
ref.ref.all_after_last('/') in s.objects.map(it.name())
}
return base_obj_is_param || base_obj_is_result
}
fn (m ActorMethod) is_new_method() bool {
return m.name.starts_with('new')
}
fn (m ActorMethod) is_get_method() bool {
return m.name.starts_with('get')
}
fn (m ActorMethod) is_set_method() bool {
return m.name.starts_with('set')
}
fn (m ActorMethod) is_delete_method() bool {
return m.name.starts_with('delete')
}
fn (m ActorMethod) is_list_method() bool {
return m.name.starts_with('list')
}
fn (m ActorMethod) is_filter_method() bool {
return m.name.starts_with('filter')
}
fn (m ActorMethod) is_crudlf_method() bool {
return m.is_new_method() ||
m.is_get_method() ||
m.is_set_method() ||
m.is_delete_method() ||
m.is_list_method() ||
m.is_filter_method()
}
pub fn (o BaseObject) name() string {
return if o.schema.id.trim_space() != '' {
o.schema.id.trim_space()
} else {o.schema.title.trim_space()}
}
fn (s ActorSpecification) is_base_object_new_method(method ActorMethod) bool {
return s.is_base_object_method(method) && method.name.starts_with('new')
}
fn (s ActorSpecification) is_base_object_get_method(method ActorMethod) bool {
return s.is_base_object_method(method) && method.name.starts_with('get')
}
fn (s ActorSpecification) is_base_object_set_method(method ActorMethod) bool {
return s.is_base_object_method(method) && method.name.starts_with('set')
}
fn (s ActorSpecification) is_base_object_delete_method(method ActorMethod) bool {
return s.is_base_object_method(method) && method.name.starts_with('delete')
}
fn (s ActorSpecification) is_base_object_list_method(method ActorMethod) bool {
return s.is_base_object_method(method) && method.name.starts_with('list')
}

View File

@@ -0,0 +1,91 @@
module specification
import freeflowuniverse.herolib.schemas.jsonschema { Schema, SchemaRef }
import freeflowuniverse.herolib.schemas.openapi { MediaType, ResponseSpec, Operation, Parameter, OpenAPI, Components, Info, PathItem, ServerSpec }
import net.http
// Converts ActorSpecification to OpenAPI
pub fn (s ActorSpecification) to_openapi() OpenAPI {
if openapi_spec := s.openapi {
return openapi_spec
}
mut paths := map[string]PathItem{}
// Map ActorMethods to paths
for method in s.methods {
op := method.to_openapi_operation()
paths['${method.http_path()}'] = match method.http_method() {
.get { PathItem {get: op} }
else { panic('unsupported http method') }
}
// Assign operation to corresponding HTTP method
// TODO: what about other verbs
}
mut schemas := map[string]SchemaRef{}
for object in s.objects {
schemas[object.schema.id] = object.to_schema()
}
return OpenAPI{
openapi: '3.0.0',
info: Info{
title: s.name,
summary: s.description,
description: s.description,
version: '1.0.0',
},
servers: [
ServerSpec{
url: 'http://localhost:8080',
description: 'Default server',
},
],
paths: paths,
components: Components{
schemas: schemas
},
}
}
fn (bo BaseObject) to_schema() Schema {
return Schema{}
}
fn (m ActorMethod) http_path() string {
return m.name
}
fn (m ActorMethod) http_method() http.Method {
return .get
}
fn (method ActorMethod) to_openapi_operation() Operation {
mut op := Operation{
summary: method.summary,
description: method.description,
operation_id: method.name,
}
// Convert parameters to OpenAPI format
for param in method.parameters {
op.parameters << Parameter{
name: param.name,
in_: 'query', // Default to query parameters; adjust based on function context
description: param.description,
required: param.required,
schema: param.schema,
}
}
// if method.is_void()
op.responses['200'] = ResponseSpec {
description: method.description
content: {
'application/json': MediaType {
schema: method.result.schema
}
}
}
return op
}

View File

@@ -0,0 +1,159 @@
module specification
import freeflowuniverse.herolib.core.code
import freeflowuniverse.herolib.schemas.jsonschema { Schema, SchemaRef }
import freeflowuniverse.herolib.schemas.openapi { Operation, Parameter, OpenAPI, Components, Info, PathItem, ServerSpec }
import freeflowuniverse.herolib.schemas.openrpc
const actor_spec = specification.ActorSpecification{
name: 'Petstore'
structure: code.Struct{
is_pub: false
}
interfaces: [.openrpc]
methods: [specification.ActorMethod{
name: 'list_pets'
summary: 'List all pets'
parameters: [openrpc.ContentDescriptor{
name: 'limit'
description: 'How many items to return at one time (max 100)'
required: false
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'integer'
minimum: 1
})
}]
result: openrpc.ContentDescriptor{
name: 'pets'
description: 'A paged array of pets'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'array'
items: jsonschema.Items(jsonschema.SchemaRef(jsonschema.Schema{
typ: 'object'
properties: {
'id': jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/PetId'
}),
'name': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
}),
'tag': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}
required: ['id', 'name']
}))
})
}
}, specification.ActorMethod{
name: 'create_pet'
summary: 'Create a pet'
parameters: [openrpc.ContentDescriptor{
name: 'newPetName'
description: 'Name of pet to create'
required: true
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}, openrpc.ContentDescriptor{
name: 'newPetTag'
description: 'Pet tag to create'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}]
}, specification.ActorMethod{
name: 'get_pet'
summary: 'Info for a specific pet'
result: openrpc.ContentDescriptor{
name: 'pet'
description: 'Expected response to a valid request'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'object'
properties: {
'id': jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/PetId'
}),
'name': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
}),
'tag': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}
required: ['id', 'name']
})
}
}, specification.ActorMethod{
name: 'update_pet'
summary: 'Update a pet'
parameters: [openrpc.ContentDescriptor{
name: 'updatedPetName'
description: 'New name for the pet'
required: true
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}, openrpc.ContentDescriptor{
name: 'updatedPetTag'
description: 'New tag for the pet'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}]
result: openrpc.ContentDescriptor{
name: 'pet'
description: 'Updated pet object'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'object'
properties: {
'id': jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/PetId'
}),
'name': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
}),
'tag': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}
required: ['id', 'name']
})
}
}, specification.ActorMethod{
name: 'delete_pet'
summary: 'Delete a pet'
result: openrpc.ContentDescriptor{
name: 'success'
description: 'Boolean indicating success'
schema: jsonschema.SchemaRef(jsonschema.Schema{
typ: 'boolean'
})
}
}]
objects: [specification.BaseObject{
schema: jsonschema.Schema{
id: 'pet'
title: 'Pet'
typ: 'object'
properties: {
'id': jsonschema.SchemaRef(jsonschema.Reference{
ref: '#/components/schemas/PetId'
}),
'name': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
}),
'tag': jsonschema.SchemaRef(jsonschema.Schema{
typ: 'string'
})
}
required: ['id', 'name']
}
}]
}
// Converts ActorSpecification to OpenAPI
pub fn test_specification_to_openapi() {
panic(actor_spec.to_openapi())
}

View File

@@ -0,0 +1,72 @@
module specification
import freeflowuniverse.herolib.schemas.openrpc {OpenRPC, Components}
import freeflowuniverse.herolib.schemas.jsonschema {SchemaRef}
import freeflowuniverse.herolib.schemas.jsonschema.codegen { struct_to_schema }
// pub fn from_openrpc(spec openrpc.OpenRPC) !ActorSpecification {
// // Extract Actor metadata from OpenRPC info
// // actor_name := openrpc_doc.info.title
// // actor_description := openrpc_doc.info.description
// // // Generate methods
// // mut methods := []ActorMethod{}
// // for method in openrpc_doc.methods {
// // method_code := method.to_code()! // Using provided to_code function
// // methods << ActorMethod{
// // name: method.name
// // func: method_code
// // }
// // }
// // // Generate BaseObject structs from schemas
// // mut objects := []BaseObject{}
// // for key, schema_ref in openrpc_doc.components.schemas {
// // struct_obj := schema_ref.to_code()! // Assuming schema_ref.to_code() converts schema to Struct
// // // objects << BaseObject{
// // // structure: code.Struct{
// // // name: struct_obj.name
// // // }
// // // }
// // }
// // Build the Actor struct
// return ActorSpecification{
// // name: actor_name
// // description: actor_description
// // methods: methods
// // objects: objects
// }
// }
pub fn (specification ActorSpecification) to_openrpc() OpenRPC {
mut schemas := map[string]SchemaRef{}
for obj in specification.objects {
schemas[obj.schema.id] = obj.schema
// for child in obj.children {
// schemas[child.name] = struct_to_schema(child)
// }
}
return OpenRPC{
info: openrpc.Info{
title: specification.name.title()
version: '1.0.0'
}
methods: specification.methods.map(method_to_openrpc_method(it))
components: Components{
schemas: schemas
}
}
}
pub fn method_to_openrpc_method(method ActorMethod) openrpc.Method {
return openrpc.Method {
name: method.name
summary: method.summary
description: method.description
params: method.parameters.map(openrpc.ContentDescriptorRef(it))
result: openrpc.ContentDescriptorRef(method.result)
errors: method.errors.map(openrpc.ErrorRef(it))
}
}

140
lib/baobab/stage/README.md Normal file
View File

@@ -0,0 +1,140 @@
# Stage Module
The **Stage** module is a core component of the **Baobab** (Base Object and Actor Backend) library. It provides the infrastructure for handling RPC-based communication and managing the lifecycle of **Actors** and **Actions**. This module facilitates processing incoming requests, converting them to actions, and ensuring their correct execution.
## Architecture Overview
The **Stage** module operates based on the following architecture:
1. **RPC Request Handling**:
- An **Interface Handler** receives an RPC request. Supported interfaces include:
- **OpenRPC**
- **JSON-RPC**
- **OpenAPI**
2. **Action Creation**:
- The **Interface Handler** converts the incoming request into an **Action**, which represents the task to be executed.
3. **Action Execution**:
- The **Interface Handler** passes the **Action** to the **Director** for coordinated execution.
- (Note: Currently, the **Director** is not fully implemented. Actions are passed directly to the **Actor** for execution.)
4. **Actor Processing**:
- The **Actor** uses its `act` method to execute the **Action**.
- The result of the **Action** is stored in its `result` field, and the **Action** is returned.
5. **RPC Response Generation**:
- The **Interface Handler** converts the resulting **Action** back into the appropriate RPC response format and returns it.
---
## Key Components
### **Interface Handlers**
- **Responsibilities**:
- Receive and parse incoming RPC requests.
- Convert requests into **Actions**.
- Convert resulting **Actions** into appropriate RPC responses.
- Files:
- `interfaces/jsonrpc_interface.v`
- `interfaces/openapi_interface.v`
### **Director**
- **Responsibilities**:
- (Planned) Coordinate the execution of **Actions**.
- Handle retries, timeouts, and error recovery.
- File:
- `director.v`
### **Actors**
- **Responsibilities**:
- Execute **Actions** using their `act` method.
- Populate the `result` field of **Actions** with the execution result.
- File:
- `actor.v`
### **Actions**
- **Responsibilities**:
- Represent tasks to be executed by **Actors**.
- Carry results back after execution.
- File:
- `action.v`
### **Executor**
- **Responsibilities**:
- Manage the assignment of **Actions** to **Actors**.
- File:
- `executor.v`
---
## Directory Structure
```
stage/
interfaces/
jsonrpc_interface.v # Converts JSON-RPC requests to Actions
openapi_interface.v # Converts OpenAPI requests to Actions
actor.v # Defines the Actor and its behavior
action.v # Defines the Action structure and utilities
executor.v # Executes Actions on Actors
director.v # (Planned) Coordinates actors, actions, and retries
```
---
## Workflow Example
### 1. Receiving an RPC Request
An RPC request is received by an interface handler:
```json
{
"jsonrpc": "2.0",
"method": "doSomething",
"params": { "key": "value" },
"id": 1
}
```
### 2. Converting the Request to an Action
The interface handler converts the request into an **Action**:
```v
action := jsonrpc_interface.jsonrpc_to_action(request)
```
### 3. Executing the Action
The action is passed directly to an **Actor** for execution:
```v
actor := MyActor{id: "actor-1"}
resulting_action := actor.act(action)
```
### 4. Returning the RPC Response
The interface handler converts the resulting **Action** back into a JSON-RPC response:
```json
{
"jsonrpc": "2.0",
"result": { "status": "success", "data": "..." },
"id": 1
}
```
---
## Future Improvements
- **Director Implementation**:
- Add retries and timeout handling for actions.
- Provide better coordination for complex workflows.
- **Enhanced Interfaces**:
- Add support for more RPC protocols.
---
This module is a crucial building block of the **Baobab** library, designed to streamline RPC-based communication and task execution with flexibility and scalability.

15
lib/baobab/stage/action.v Normal file
View File

@@ -0,0 +1,15 @@
module stage
// import freeflowuniverse.herolib.core.smartid
pub struct Action {
pub mut:
id string
name string
priority int = 10 // 0 is highest, do 10 as default
params string // json encoded params
result string // can be used to remember outputs
// run bool = true // certain actions can be defined but meant to be executed directly
comments string
done bool // if done then no longer need to process
}

View File

@@ -0,0 +1,47 @@
module stage
import freeflowuniverse.herolib.core.redisclient
// Processor struct for managing procedure calls
pub struct Client {
pub mut:
rpc redisclient.RedisRpc // Redis RPC mechanism
}
// Parameters for processing a procedure call
@[params]
pub struct Params {
pub:
timeout int // Timeout in seconds
}
pub struct ClientConfig {
ActorConfig
pub:
redis_url string = 'localhost:6379' // url to redis server running
}
pub fn new_client(config ActorConfig) !Client {
mut redis := redisclient.new(config.redis_url)!
mut rpc_q := redis.rpc_get(config.redis_queue_name())
return Client{
rpc: rpc_q
}
}
// Process the procedure call
pub fn (mut p Client) call_to_action(action Action, params Params) !Action {
// Use RedisRpc's `call` to send the call and wait for the response
response_data := p.rpc.call(redisclient.RPCArgs{
cmd: action.name
data: action.params
timeout: u64(params.timeout * 1000) // Convert seconds to milliseconds
wait: true
})!
return Action {
...action
result: response_data
}
}

77
lib/baobab/stage/actor.v Normal file
View File

@@ -0,0 +1,77 @@
module stage
import freeflowuniverse.herolib.baobab.osis {OSIS}
import freeflowuniverse.herolib.core.redisclient
@[heap]
pub interface IActor {
name string
mut:
act(Action) !Action
}
pub struct Actor {
ActorConfig
mut:
osis OSIS
}
@[params]
pub struct ActorConfig {
pub:
name string
version string
redis_url string = 'localhost:6379'
}
pub fn (config ActorConfig) redis_queue_name() string {
mut str := 'actor_${config.name}'
if config.version != '' {
str += '_${config.version}'
}
return str
}
pub fn new_actor(config ActorConfig) !Actor {
return Actor{
ActorConfig: config
osis: osis.new()!
}
}
pub fn (a ActorConfig) get_redis_rpc() !redisclient.RedisRpc {
mut redis := redisclient.new(a.redis_url)!
return redis.rpc_get(a.redis_queue_name())
}
pub fn (a ActorConfig) version(v string) ActorConfig {
return ActorConfig {...a,
version: v
}
}
pub fn (a ActorConfig) example() ActorConfig {
return ActorConfig {...a,
version: 'example'
}
}
pub fn (mut a IActor) handle(method string, data string) !string {
action := a.act(
name: method
params: data
)!
return action.result
}
// // Actor listens to the Redis queue for method invocations
// pub fn (mut a IActor) run() ! {
// mut redis := redisclient.new('localhost:6379') or { panic(err) }
// mut rpc := redis.rpc_get(a.name)
// println('Actor started and listening for tasks...')
// for {
// rpc.process(a.handle)!
// time.sleep(time.millisecond * 100) // Prevent CPU spinning
// }
// }

View File

@@ -0,0 +1,2 @@
module stage

Some files were not shown because too many files have changed in this diff Show More