diff --git a/backend/Manifest.toml b/backend/Manifest.toml index 6321e62e..0e1cd379 100644 --- a/backend/Manifest.toml +++ b/backend/Manifest.toml @@ -1,8 +1,8 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.11.4" +julia_version = "1.11.5" manifest_format = "2.0" -project_hash = "0db7880fbd4546921607752894b504bef3ed2e59" +project_hash = "1815c987b48f906b5fa93839578ef5a15ffb6bb8" [[deps.ArgTools]] uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" @@ -32,13 +32,15 @@ deps = ["TOML", "UUIDs"] git-tree-sha1 = "8ae8d32e09f0dcf42a36b90d4e17f5dd2e4c4215" uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" version = "4.16.0" +weakdeps = ["Dates", "LinearAlgebra"] [deps.Compat.extensions] CompatLinearAlgebraExt = "LinearAlgebra" - [deps.Compat.weakdeps] - Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" - LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +[[deps.CompilerSupportLibraries_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" +version = "1.1.1+0" [[deps.ConcurrentUtilities]] deps = ["Serialization", "Sockets"] @@ -46,6 +48,12 @@ git-tree-sha1 = "d9d26935a0bcffc87d2613ce14c527c99fc543fd" uuid = "f0e56b4a-5159-44fe-b623-3e5288b988bb" version = "2.5.0" +[[deps.DataStructures]] +deps = ["Compat", "InteractiveUtils", "OrderedCollections"] +git-tree-sha1 = "4e1fe97fdaed23e9dc21d4d664bea76b65fc50a0" +uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +version = "0.18.22" + [[deps.Dates]] deps = ["Printf"] uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" @@ -145,6 +153,11 @@ version = "1.11.0+1" uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" version = "1.11.0" +[[deps.LinearAlgebra]] +deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"] +uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +version = "1.11.0" + [[deps.Logging]] uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" version = "1.11.0" @@ -200,6 +213,11 @@ git-tree-sha1 = "cf7c2bf104f484f7c9b394c8d32f76d994604ba4" uuid = "d5e62ea6-ddf3-4d43-8e4c-ad5e6c8bfd7d" version = "0.2.0" +[[deps.OpenBLAS_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] +uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" +version = "0.3.27+1" + [[deps.OpenSSL]] deps = ["BitFlags", "Dates", "MozillaCACerts_jll", "OpenSSL_jll", "Sockets"] git-tree-sha1 = "f1a7e086c677df53e064e0fdd2c9d0b0833e3f6e" @@ -212,6 +230,35 @@ git-tree-sha1 = "9216a80ff3682833ac4b733caa8c00390620ba5d" uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" version = "3.5.0+0" +[[deps.OrderedCollections]] +git-tree-sha1 = "05868e21324cede2207c6f0f466b4bfef6d5e7ee" +uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +version = "1.8.1" + +[[deps.Oxygen]] +deps = ["DataStructures", "Dates", "HTTP", "JSON3", "MIMEs", "Reexport", "RelocatableFolders", "Sockets", "Statistics", "StructTypes"] +git-tree-sha1 = "7aab89647a9523dc62ac2cc8e844d097b9c1aa47" +uuid = "df9a0d86-3283-4920-82dc-4555fc0d1d8b" +version = "1.7.2" + + [deps.Oxygen.extensions] + BonitoExt = "Bonito" + CairoMakieExt = "CairoMakie" + MustacheExt = "Mustache" + OteraEngineExt = "OteraEngine" + ProtoBufExt = "ProtoBuf" + TimeZonesExt = "TimeZones" + WGLMakieExt = ["WGLMakie", "Bonito"] + + [deps.Oxygen.weakdeps] + Bonito = "824d6782-a2ef-11e9-3a09-e5662e0c26f8" + CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" + Mustache = "ffc61752-8dc7-55ee-8c37-f3e9cdd09e70" + OteraEngine = "b2d7f28f-acd6-4007-8b26-bc27716e5513" + ProtoBuf = "3349acd9-ac6a-5e09-bcdb-63829b23a429" + TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53" + WGLMakie = "276b4fcb-3e11-5398-bf8b-a0c2d153d008" + [[deps.Parsers]] deps = ["Dates", "PrecompileTools", "UUIDs"] git-tree-sha1 = "7d2f8f21da5db6a806faf7b9b292296da42b2810" @@ -240,6 +287,17 @@ deps = ["SHA"] uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" version = "1.11.0" +[[deps.Reexport]] +git-tree-sha1 = "45e428421666073eab6f2da5c9d310d99bb12f9b" +uuid = "189a3867-3050-52da-a836-e630ba90ab69" +version = "1.2.2" + +[[deps.RelocatableFolders]] +deps = ["SHA", "Scratch"] +git-tree-sha1 = "ffdaf70d81cf6ff22c2b6e733c900c3321cab864" +uuid = "05181044-ff0b-4ac5-8273-598c1e38db00" +version = "1.0.1" + [[deps.SHA]] uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" version = "0.7.0" @@ -263,6 +321,18 @@ version = "1.2.0" uuid = "6462fe0b-24de-5631-8697-dd941f90decc" version = "1.11.0" +[[deps.Statistics]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "ae3bb1eb3bba077cd276bc5cfc337cc65c3075c0" +uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +version = "1.11.1" + + [deps.Statistics.extensions] + SparseArraysExt = ["SparseArrays"] + + [deps.Statistics.weakdeps] + SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" + [[deps.StructTypes]] deps = ["Dates", "UUIDs"] git-tree-sha1 = "159331b30e94d7b11379037feeb9b690950cace8" @@ -321,6 +391,11 @@ deps = ["Libdl"] uuid = "83775a58-1f1d-513f-b197-d71354ab007a" version = "1.2.13+1" +[[deps.libblastrampoline_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" +version = "5.11.0+0" + [[deps.nghttp2_jll]] deps = ["Artifacts", "Libdl"] uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" diff --git a/backend/Project.toml b/backend/Project.toml index a382ac12..7fc1dbd8 100644 --- a/backend/Project.toml +++ b/backend/Project.toml @@ -10,7 +10,11 @@ HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" OpenAPI = "d5e62ea6-ddf3-4d43-8e4c-ad5e6c8bfd7d" +Oxygen = "df9a0d86-3283-4920-82dc-4555fc0d1d8b" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +Reexport = "189a3867-3050-52da-a836-e630ba90ab69" +StructTypes = "856f2bd8-1eba-4b0a-8007-ebc267875bd4" +TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53" URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" @@ -24,6 +28,10 @@ HTTP = "1.9.14" JSON = "0.21.4" JSON3 = "1.14.3" OpenAPI = "0.2.0" +Oxygen = "1.7.2" Random = "1.11.0" +Reexport = "1.2.2" +StructTypes = "1.11.0" +TOML = "1.0.3" TimeZones = "1.21.3" URIs = "1.5.2" diff --git a/backend/config/agents.toml b/backend/config/agents.toml new file mode 100644 index 00000000..211050cc --- /dev/null +++ b/backend/config/agents.toml @@ -0,0 +1,39 @@ +# JuliaOS Agents Configuration + +# Storage settings +[storage] +path = "db/agents_state.json" +backup_enabled = true +backup_count = 5 +auto_persist = true + +# Agent settings +[agent] +max_task_history = 100 +xp_decay_rate = 0.999 +default_sleep_ms = 1000 +paused_sleep_ms = 500 +auto_restart = false +monitoring_enabled = true +monitor_interval = 30 +max_stall_seconds = 300 + +# Metrics settings +[metrics] +enabled = true +collection_interval = 60 +retention_period = 86400 # 24 hours in seconds + +# Swarm settings +[swarm] +enabled = true +backend = "memory" # Options: none, memory, redis, nats, zeromq +connection_string = "" +default_topic = "juliaos.swarm" + +# LLM settings +[llm] +default_provider = "openai" +default_model = "gpt-4o-mini" +default_temperature = 0.7 +default_max_tokens = 1024 diff --git a/backend/config/config.example.toml b/backend/config/config.example.toml new file mode 100644 index 00000000..e4d5d067 --- /dev/null +++ b/backend/config/config.example.toml @@ -0,0 +1,75 @@ +# JuliaOS Configuration Example +# Copy this file to config.toml and update with your own values + +# API configuration +[api] +host = "0.0.0.0" +port = 8052 +log_level = "info" + +# Storage configuration +[storage] +local_db_path = "~/.juliaos/juliaos.sqlite" +# Arweave configuration for decentralized storage +arweave_wallet_file = "" # Path to your Arweave wallet JSON file +arweave_gateway = "arweave.net" +arweave_port = 443 +arweave_protocol = "https" +arweave_timeout = 20000 +arweave_logging = false + +# Blockchain configuration +[blockchain] +default_chain = "ethereum" +# Replace these with your own RPC URLs +rpc_urls.ethereum = "https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY" # Get your key at https://infura.io +rpc_urls.polygon = "https://polygon-rpc.com" +rpc_urls.solana = "https://api.mainnet-beta.solana.com" +max_gas_price = 100.0 # Maximum gas price in GWEI +max_slippage = 0.01 # Maximum slippage for swaps (1%) +supported_chains = ["ethereum", "polygon", "solana"] + +# Swarm configuration +[swarm] +default_algorithm = "DE" # Differential Evolution +default_population_size = 50 +max_iterations = 1000 +parallel_evaluation = true + +# Security configuration +[security] +rate_limit = 100 # requests per minute +max_request_size = 1048576 # 1MB +enable_authentication = true # Set to true to enable API key authentication +# Add a list of valid API keys. Clients must provide one of these in the X-API-Key header. +# It is strongly recommended to change these default keys and use secure, randomly generated keys. +api_keys = ["your-secure-api-key-1", "your-secure-api-key-2"] + +# Bridge configuration +[bridge] +port = 8052 +host = "localhost" +bridge_api_url = "http://localhost:3001/api/v1" + +# Wormhole bridge configuration +[wormhole] +enabled = true +network = "testnet" # "mainnet" or "testnet" + +# Ethereum network configuration for Wormhole +[wormhole.networks.ethereum] +enabled = true +rpcUrl = "https://goerli.infura.io/v3/YOUR_INFURA_API_KEY" # For testnet (Goerli) +# rpcUrl = "https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY" # For mainnet + +# Solana network configuration for Wormhole +[wormhole.networks.solana] +enabled = true +rpcUrl = "https://api.devnet.solana.com" # For testnet (Devnet) +# rpcUrl = "https://api.mainnet-beta.solana.com" # For mainnet + +# Logging configuration +[logging] +level = "info" # debug, info, warn, error +format = "json" +retention_days = 7 diff --git a/backend/config/config.jl b/backend/config/config.jl new file mode 100644 index 00000000..49dfc84e --- /dev/null +++ b/backend/config/config.jl @@ -0,0 +1,214 @@ +module Config + +using TOML # Add TOML package import + +export load, get_value + +# Default configuration +const DEFAULT_CONFIG = Dict( + "api" => Dict( + "host" => "127.0.0.1", + "port" => 8052, + "log_level" => "info" + ), + "storage" => Dict( + "local_db_path" => joinpath(homedir(), ".juliaos", "juliaos.sqlite"), + "arweave_wallet_file" => get(ENV, "ARWEAVE_WALLET_FILE", ""), + "arweave_gateway" => get(ENV, "ARWEAVE_GATEWAY", "arweave.net"), + "arweave_port" => parse(Int, get(ENV, "ARWEAVE_PORT", "443")), + "arweave_protocol" => get(ENV, "ARWEAVE_PROTOCOL", "https"), + "arweave_timeout" => parse(Int, get(ENV, "ARWEAVE_TIMEOUT", "20000")), + "arweave_logging" => parse(Bool, get(ENV, "ARWEAVE_LOGGING", "false")) + ), + "blockchain" => Dict( + "default_chain" => "ethereum", + "rpc_urls" => Dict( + "ethereum" => "https://mainnet.infura.io/v3/YOUR_API_KEY", + "polygon" => "https://polygon-rpc.com", + "solana" => "https://api.mainnet-beta.solana.com" + ), + "max_gas_price" => 100.0, + "max_slippage" => 0.01, + "supported_chains" => ["ethereum", "polygon", "solana"] + ), + "swarm" => Dict( + "default_algorithm" => "DE", + "default_population_size" => 50, + "max_iterations" => 1000, + "parallel_evaluation" => true + ), + "security" => Dict( + "rate_limit" => 100, # requests per minute + "max_request_size" => 1048576, # 1MB + "enable_authentication" => true, # Enabled by default + "api_keys" => ["default-secret-key-please-change"] # List of valid API keys + ), + "bridge" => Dict( + "port" => 8052, + "host" => "localhost", + "bridge_api_url" => "http://localhost:3001/api/v1" + ), + + "wormhole" => Dict( + "enabled" => true, + "network" => "testnet", + "networks" => Dict( + "ethereum" => Dict( + "rpcUrl" => "https://goerli.infura.io/v3/your-infura-key", + "enabled" => true + ), + "solana" => Dict( + "rpcUrl" => "https://api.devnet.solana.com", + "enabled" => true + ) + ) + ), + "logging" => Dict( + "level" => "info", + "format" => "json", + "retention_days" => 7 + ) +) + +# Configuration object with dot notation access +struct Configuration + data::Dict{String, Any} + + # Constructor that allows dot notation access to nested dictionaries + function Configuration(data::Dict) + new(convert(Dict{String, Any}, data)) + end + + # Allow dot notation access + function Base.getproperty(config::Configuration, key::Symbol) + key_str = String(key) + if key_str == "data" + return getfield(config, :data) + elseif haskey(config.data, key_str) + value = config.data[key_str] + if value isa Dict{String, Any} + return Configuration(value) + else + return value + end + else + error("Configuration key not found: $key_str") + end + end + + # Check if a key exists + function Base.haskey(config::Configuration, key::Symbol) + key_str = String(key) + return haskey(config.data, key_str) + end +end + +""" + load(config_path=nothing) + +Load configuration from environment variables and optionally from a TOML file. +Environment variables take precedence over file configuration. +""" +function load(config_path=nothing) + # Start with default configuration + config_data = deepcopy(DEFAULT_CONFIG) + println("Starting to load configuration...") + + # Load from file if provided + if !isnothing(config_path) && isfile(config_path) + println("Attempting to load configuration from specified path: $config_path") + try + file_config = TOML.parsefile(config_path) + println("Successfully loaded configuration from specified path") + merge_configs!(config_data, file_config) + catch e + @warn "Failed to load configuration from specified path: $e" + end + elseif isfile(joinpath(@__DIR__, "config.toml")) + println("Attempting to load configuration from default path: $(joinpath(@__DIR__, "config.toml"))") + try + file_config = TOML.parsefile(joinpath(@__DIR__, "config.toml")) + println("Successfully loaded configuration from default path") + merge_configs!(config_data, file_config) + catch e + @warn "Failed to load configuration from default path: $e" + end + else + println("No configuration file found") + end + + # Override with environment variables + println("Starting to load configuration from environment variables...") + override_from_env!(config_data) + + return Configuration(config_data) +end + +""" + merge_configs!(target, source) + +Recursively merge source configuration into target. +""" +function merge_configs!(target::Dict, source::Dict) + for (key, value) in source + if haskey(target, key) && target[key] isa Dict && value isa Dict + merge_configs!(target[key], value) + else + target[key] = value + end + end +end + +""" + override_from_env!(config) + +Override configuration values from environment variables. +Environment variables should be in the format JULIAOS_SECTION_KEY. +""" +function override_from_env!(config::Dict) + for (env_key, env_value) in ENV + if startswith(env_key, "JULIAOS_") + parts = split(env_key[9:end], "_") + if length(parts) >= 2 + section = lowercase(parts[1]) + key = lowercase(join(parts[2:end], "_")) + + if haskey(config, section) && haskey(config[section], key) + # Convert value to the appropriate type + original_value = config[section][key] + if original_value isa Bool + config[section][key] = lowercase(env_value) in ["true", "1", "yes"] + elseif original_value isa Integer + config[section][key] = parse(Int, env_value) + elseif original_value isa AbstractFloat + config[section][key] = parse(Float64, env_value) + else + config[section][key] = env_value + end + end + end + end + end +end + +""" + get_value(config::Configuration, path::String, default=nothing) + +Get a configuration value by path (e.g., "server.port"). +Returns the default value if the path doesn't exist. +""" +function get_value(config::Configuration, path::String, default=nothing) + parts = split(path, ".") + current = config + + for part in parts + if !haskey(current, Symbol(part)) + return default + end + current = getproperty(current, Symbol(part)) + end + + return current +end + +end # module diff --git a/backend/config/config.toml b/backend/config/config.toml new file mode 100644 index 00000000..95ca4a03 --- /dev/null +++ b/backend/config/config.toml @@ -0,0 +1,156 @@ +# JuliaOS Configuration + +[system] +environment = "development" # development, staging, production +debug = true +log_level = "info" # debug, info, warn, error +version = "0.1.0" + +[api] +host = "0.0.0.0" +port = 8052 +log_level = "info" +cors_enabled = true +allowed_origins = ["*"] +rate_limit = 100 # requests per minute +timeout = 30 # seconds + +[api.websocket] +enabled = true +port = 8053 +heartbeat_interval = 30 # seconds + +# Security configuration +[security] +rate_limit = 100 # requests per minute +max_request_size = 1048576 # 1MB +enable_authentication = false +# Add a list of valid API keys. Clients must provide one of these in the X-API-Key header. +# It is strongly recommended to change these default keys and use secure, randomly generated keys. +api_keys = ["default-secret-key-please-change"] + +[storage] +type = "local" # local, arweave, web3 +path = "data/storage" +max_file_size = 10485760 # 10MB in bytes +local_db_path = "~/.juliaos/juliaos.sqlite" +arweave_wallet_file = "" +arweave_gateway = "arweave.net" +arweave_port = 443 +arweave_protocol = "https" +arweave_timeout = 20000 +arweave_logging = false + +[storage.arweave] +host = "arweave.net" +port = 443 +protocol = "https" + +[storage.web3] +ipfs_gateway = "https://ipfs.io/ipfs/" +pinata_api_key = "" +pinata_secret_key = "" + +[database] +type = "sqlite" # sqlite, postgres +path = "data/juliaos.db" +pool_size = 5 +timeout = 30 # seconds + +[cache] +type = "memory" # memory, redis +max_size = 1000 # entries +ttl = 3600 # seconds + +[cache.redis] +host = "localhost" +port = 6379 +password = "" +db = 0 + +[queue] +type = "memory" # memory, redis +max_size = 10000 # entries + +[blockchain] +default_chain = "ethereum" +rpc_urls.ethereum = "https://mainnet.infura.io/v3/YOUR_API_KEY" +rpc_urls.polygon = "https://polygon-rpc.com" +rpc_urls.solana = "https://api.mainnet-beta.solana.com" +max_gas_price = 100.0 +max_slippage = 0.01 +supported_chains = ["ethereum", "polygon", "solana"] +gas_limit = 300000 +gas_price_strategy = "medium" # low, medium, high + +[blockchain.networks.ethereum] +rpc_url = "https://mainnet.infura.io/v3/your-project-id" +chain_id = 1 +explorer = "https://etherscan.io" + +[blockchain.networks.polygon] +rpc_url = "https://polygon-rpc.com" +chain_id = 137 +explorer = "https://polygonscan.com" + +[agents] +max_agents = 100 +default_timeout = 300 # seconds +health_check_interval = 60 # seconds + +[swarms] +max_swarms = 10 +max_agents_per_swarm = 50 +optimization_timeout = 600 # seconds + +[bridges] +default_bridge = "wormhole" +timeout = 300 # seconds +confirmations = 12 # blocks + +[bridges.wormhole] +contract = "0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B" +guardian_rpc = "https://wormhole-v2-mainnet-api.certus.one" + +[bridges.axelar] +gateway = "0x4F4495243837681061C4743b74B3eEdf548D56A5" +gas_service = "0x2d5d7d31F671F86C782533cc367F14109a082712" + +[monitoring] +enable_metrics = true +metrics_port = 9090 +enable_tracing = true +trace_sample_rate = 0.1 # 10% of requests + +[development] +hot_reload = true +debug_logging = true +profile_endpoints = false + +[swarm] +default_algorithm = "DE" +default_population_size = 50 +max_iterations = 1000 +parallel_evaluation = true + +[bridge] +port = 8052 +host = "localhost" +bridge_api_url = "http://localhost:3001/api/v1" + +[wormhole] +enabled = true +network = "testnet" + +[wormhole.networks.ethereum] +rpcUrl = "https://goerli.infura.io/v3/your-infura-key" +enabled = true + +[wormhole.networks.solana] +rpcUrl = "https://api.devnet.solana.com" +enabled = true + +[logging] +level = "info" +format = "json" +retention_days = 7 \ No newline at end of file diff --git a/backend/package.json b/backend/package.json new file mode 100755 index 00000000..8e97698c --- /dev/null +++ b/backend/package.json @@ -0,0 +1,16 @@ +{ + "name": "juliaos-web-cli", + "version": "0.1.0", + "description": "JuliaOS Framework Web - Web interface for JuliaOS framework", + "scripts": { + "server": "julia --threads 4,1 --project=. run_server.jl", + "server2": "julia --threads 4,1 --project=. src/api/JuliaOSV1Server.jl", + "test:all": "julia --project=. test/runtests.jl", + "test:file": "julia --project=. -e \"include(ARGS[1])\"", + "deps:install": "julia --project=. -e \"using Pkg; Pkg.instantiate()\"", + "deps:update": "julia --project=. -e \"using Pkg; Pkg.update()\"", + "deps:add": "julia --project=. -e \"using Pkg; Pkg.add(ARGS[1])\"" + }, + "author": "JuliaOS Team", + "license": "MIT" +} \ No newline at end of file diff --git a/backend/run_server.jl b/backend/run_server.jl index 8377de6f..9b0a2991 100644 --- a/backend/run_server.jl +++ b/backend/run_server.jl @@ -6,7 +6,7 @@ using JuliaOSBackend.JuliaOSV1Server function main() host = get(ENV, "HOST", "127.0.0.1") port = parse(Int, get(ENV, "PORT", "8052")) - JuliaOSV1Server.run_server(host, port) + JuliaOSV1Server.start_server(api_host=host, api_port=port) end if abspath(PROGRAM_FILE) == @__FILE__ diff --git a/backend/src/JuliaOSBackend.jl b/backend/src/JuliaOSBackend.jl index 2ccb2f17..9f02ec8b 100644 --- a/backend/src/JuliaOSBackend.jl +++ b/backend/src/JuliaOSBackend.jl @@ -1,10 +1,13 @@ module JuliaOSBackend + +include("agents/CommonTypes.jl") include("resources/Resources.jl") include("agents/Agents.jl") include("api/JuliaOSV1Server.jl") using .Resources +using .CommonTypes using .Agents using .JuliaOSV1Server diff --git a/backend/src/api/AgentHandlers.jl b/backend/src/api/AgentHandlers.jl new file mode 100644 index 00000000..46541531 --- /dev/null +++ b/backend/src/api/AgentHandlers.jl @@ -0,0 +1,99 @@ +module AgentHandlers + +using HTTP + +using ..JuliaOSServer +using ..Agents: Agents, Triggers + +@registerAgentApi function create_agent(req::HTTP.Request, create_agent_request::CreateAgentRequest;)::AgentSummary + @info "Triggered endpoint: POST /agents" + + id = create_agent_request.id + received_blueprint = create_agent_request.blueprint + + tools = Vector{Agents.ToolBlueprint}() + for tool in received_blueprint.tools + push!(tools, Agents.ToolBlueprint(tool.name, tool.config)) + end + + trigger_type = Triggers.trigger_name_to_enum(received_blueprint.trigger.type) + trigger_params = Triggers.process_trigger_params(trigger_type, received_blueprint.trigger.params) + + internal_blueprint = Agents.AgentBlueprint( + tools, + Agents.StrategyBlueprint(received_blueprint.strategy.name, received_blueprint.strategy.config), + Agents.CommonTypes.TriggerConfig(trigger_type, trigger_params) + ) + + agent = Agents.create_agent(id, internal_blueprint) + @info "Created agent: $(agent.id) with state: $(agent.state)" + return AgentSummary(agent.id, Agents.agent_state_to_string(agent.state)) +end + +@registerAgentApi function delete_agent(req::HTTP.Request, agent_id::String;)::Nothing + @info "Triggered endpoint: DELETE /agents/$(agent_id)" + Agents.delete_agent(agent_id) + @info "Deleted agent $(agent_id)" + return nothing +end + +@registerAgentApi function update_agent(req::HTTP.Request, agent_id::String, agent_update::AgentUpdate;)::AgentSummary + @info "Triggered endpoint: PUT /agents/$(agent_id)" + agent = get(Agents.AGENTS, agent_id) do + error("Agent $(agent_id) does not exist!") + end + new_state = Agents.string_to_agent_state(agent_update.state) + Agents.set_agent_state(agent, new_state) + return AgentSummary(agent.id, Agents.agent_state_to_string(agent.state)) +end + +@registerAgentApi function get_agent(req::HTTP.Request, agent_id::String;)::AgentSummary + @info "Triggered endpoint: GET /agents/$(agent_id)" + agent = get(Agents.AGENTS, agent_id) do + error("Agent $(agent_id) does not exist!") + end + return AgentSummary(agent.id, Agents.agent_state_to_string(agent.state)) +end + +@registerAgentApi function list_agents(req::HTTP.Request;)::Vector{AgentSummary} + @info "Triggered endpoint: GET /agents" + agents = Vector{AgentSummary}() + for (id, agent) in Agents.AGENTS + push!(agents, AgentSummary(id, Agents.agent_state_to_string(agent.state))) + end + return agents +end + +@registerAgentApi function process_agent_webhook(req::HTTP.Request, agent_id::String; request_body::Dict{String,Any}=Dict{String,Any}(),)::Nothing + @info "Triggered endpoint: POST /agents/$(agent_id)/webhook" + agent = get(Agents.AGENTS, agent_id) do + error("Agent $(agent_id) does not exist!") + end + if agent.trigger.type == Agents.CommonTypes.WEBHOOK_TRIGGER + @info "Triggering agent $(agent_id) by webhook" + if !isempty(request_body) + @info "Passing payload to agent $(agent_id) webhook: $(request_body)" + Agents.run(agent, request_body) + else + Agents.run(agent) + end + end + return nothing +end + +@registerAgentApi function get_agent_logs(req::HTTP.Request, agent_id::String;)::Dict{String, Any} + @info "Triggered endpoint: GET /agents/$(agent_id)/logs" + agent = get(Agents.AGENTS, agent_id) do + error("Agent $(agent_id) does not exist!") + end + # TODO: implement pagination + return Dict{String, Any}("logs" => agent.context.logs) +end + +@registerAgentApi function get_agent_output(req::HTTP.Request, agent_id::String;)::Dict{String, Any} + @info "Triggered endpoint: GET /agents/$(agent_id)/output" + @info "NYI, not actually getting agent $(agent_id) output..." + return Dict{String, Any}() +end + +end # module JuliaOSV1Server \ No newline at end of file diff --git a/backend/src/api/JuliaOSV1Server.jl b/backend/src/api/JuliaOSV1Server.jl index ea009a2d..bf1cd601 100644 --- a/backend/src/api/JuliaOSV1Server.jl +++ b/backend/src/api/JuliaOSV1Server.jl @@ -1,139 +1,52 @@ -module JuliaOSV1Server - +# Main entry point and server configuration for the Julia backend application. +using Oxygen using HTTP - -include("server/src/JuliaOSServer.jl") -include("openapi_server_extensions.jl") - -using .JuliaOSServer -using ..Agents: Agents, Triggers - -const server = Ref{Any}(nothing) - -function ping(::HTTP.Request) - @info "Triggered endpoint: GET /ping" - return HTTP.Response(200, "") -end - -function create_agent(req::HTTP.Request, create_agent_request::CreateAgentRequest;)::AgentSummary - @info "Triggered endpoint: POST /agents" - - id = create_agent_request.id - received_blueprint = create_agent_request.blueprint - - tools = Vector{Agents.ToolBlueprint}() - for tool in received_blueprint.tools - push!(tools, Agents.ToolBlueprint(tool.name, tool.config)) - end - - trigger_type = Triggers.trigger_name_to_enum(received_blueprint.trigger.type) - trigger_params = Triggers.process_trigger_params(trigger_type, received_blueprint.trigger.params) - - internal_blueprint = Agents.AgentBlueprint( - tools, - Agents.StrategyBlueprint(received_blueprint.strategy.name, received_blueprint.strategy.config), - Agents.CommonTypes.TriggerConfig(trigger_type, trigger_params) - ) - - agent = Agents.create_agent(id, internal_blueprint) - @info "Created agent: $(agent.id) with state: $(agent.state)" - return AgentSummary(agent.id, Agents.agent_state_to_string(agent.state)) -end - -function delete_agent(req::HTTP.Request, agent_id::String;)::Nothing - @info "Triggered endpoint: DELETE /agents/$(agent_id)" - Agents.delete_agent(agent_id) - @info "Deleted agent $(agent_id)" - return nothing -end - -function update_agent(req::HTTP.Request, agent_id::String, agent_update::AgentUpdate;)::AgentSummary - @info "Triggered endpoint: PUT /agents/$(agent_id)" - agent = get(Agents.AGENTS, agent_id) do - error("Agent $(agent_id) does not exist!") - end - new_state = Agents.string_to_agent_state(agent_update.state) - Agents.set_agent_state(agent, new_state) - return AgentSummary(agent.id, Agents.agent_state_to_string(agent.state)) -end - -function get_agent(req::HTTP.Request, agent_id::String;)::AgentSummary - @info "Triggered endpoint: GET /agents/$(agent_id)" - agent = get(Agents.AGENTS, agent_id) do - error("Agent $(agent_id) does not exist!") - end - return AgentSummary(agent.id, Agents.agent_state_to_string(agent.state)) -end - -function list_agents(req::HTTP.Request;)::Vector{AgentSummary} - @info "Triggered endpoint: GET /agents" - agents = Vector{AgentSummary}() - for (id, agent) in Agents.AGENTS - push!(agents, AgentSummary(id, Agents.agent_state_to_string(agent.state))) - end - return agents -end - -function process_agent_webhook(req::HTTP.Request, agent_id::String; request_body::Dict{String,Any}=Dict{String,Any}(),)::Nothing - @info "Triggered endpoint: POST /agents/$(agent_id)/webhook" - agent = get(Agents.AGENTS, agent_id) do - error("Agent $(agent_id) does not exist!") - end - if agent.trigger.type == Agents.CommonTypes.WEBHOOK_TRIGGER - @info "Triggering agent $(agent_id) by webhook" - if !isempty(request_body) - @info "Passing payload to agent $(agent_id) webhook: $(request_body)" - Agents.run(agent, request_body) - else - Agents.run(agent) - end - end - return nothing -end - -function get_agent_logs(req::HTTP.Request, agent_id::String;)::Dict{String, Any} - @info "Triggered endpoint: GET /agents/$(agent_id)/logs" - agent = get(Agents.AGENTS, agent_id) do - error("Agent $(agent_id) does not exist!") +using JSON3 +using Reexport + +include("RouterManager.jl"); @reexport using .RouterManager: logging_middleware,cors_middleware,auth_middleware +include("server/src/JuliaOSServer.jl"); @reexport using .JuliaOSServer + +""" + start_server(; api_host::String="0.0.0.0", api_port::Int=8000) + +Configures and starts the Oxygen HTTP server for the API. +""" +function start_server(; api_host::String="0.0.0.0", api_port::Int=8052) + # api_host = MainAppConfig.get_value(APP_CONFIG, "api.host", api_host) + # api_port = MainAppConfig.get_value(APP_CONFIG, "api.port", api_port) + @info "Initializing API server on $api_host:$api_port..." + @info "Using API host: $api_host and port: $api_port" + + server_middleware = [ + logging_middleware, + cors_middleware, + auth_middleware, + ] + try + JuliaOSServer.serve(; host=api_host, port=api_port, async=false, middleware=server_middleware, serialize=false) + @info "API server stopped." + catch e + @error "API server failed to start or crashed." exception=(e, catch_backtrace()) end - # TODO: implement pagination - return Dict{String, Any}("logs" => agent.context.logs) end -function get_agent_output(req::HTTP.Request, agent_id::String;)::Dict{String, Any} - @info "Triggered endpoint: GET /agents/$(agent_id)/output" - @info "NYI, not actually getting agent $(agent_id) output..." - return Dict{String, Any}() -end -function list_strategies(req::HTTP.Request;)::Vector{StrategySummary} - @info "Triggered endpoint: GET /strategies" - strategies = Vector{StrategySummary}() - for (name, spec) in Agents.Strategies.STRATEGY_REGISTRY - push!(strategies, StrategySummary(name)) - end - return strategies -end +function main() + @info "Starting Julia Agent Backend System..." -function list_tools(req::HTTP.Request;)::Vector{ToolSummary} - @info "Triggered endpoint: GET /tools" - tools = Vector{ToolSummary}() - for (name, tool) in Agents.Tools.TOOL_REGISTRY - push!(tools, ToolSummary(name, ToolSummaryMetadata(tool.metadata.description))) - end - return tools -end - -function run_server(host::AbstractString="0.0.0.0", port::Integer=8052) try - router = HTTP.Router() - router = JuliaOSServer.register(router, @__MODULE__; path_prefix="/api/v1") - HTTP.register!(router, "GET", "/ping", ping) - server[] = HTTP.serve!(router, host, port) - wait(server[]) - catch ex - @error("Server error", exception=(ex, catch_backtrace())) + start_server() + catch e + @error "Failed to start the API server or server crashed." exception=(e, catch_backtrace()) end + + @info "Julia Agent Backend has shut down." end -end # module JuliaOSV1Server \ No newline at end of file +# Run the main function if this script is executed directly +if abspath(PROGRAM_FILE) == @__FILE__ + main() +else + @info "Backend modules loaded. Call main() to start the server." +end \ No newline at end of file diff --git a/backend/src/api/RouterManager.jl b/backend/src/api/RouterManager.jl new file mode 100644 index 00000000..306b8197 --- /dev/null +++ b/backend/src/api/RouterManager.jl @@ -0,0 +1,106 @@ +module RouterManager + +using HTTP +using JSON3 +using OpenAPI +using Reexport +using OpenAPI +using Oxygen + +include("../../config/config.jl");@reexport using .Config: load +MainAppConfig = Config +# Load the main application configuration once +const APP_CONFIG = load(joinpath(@__DIR__, "..", "..", "config", "config.toml")) + +include("../resources/Resources.jl") +include("../agents/CommonTypes.jl") +include("../agents/Triggers.jl") +include("../agents/Agents.jl") + +include("server/src/JuliaOSServer.jl"); @reexport using .JuliaOSServer + +include("AgentHandlers.jl") +include("StrategiesHandlers.jl") +include("ToolHandlers.jl") + +using .Resources +using .CommonTypes +using .Triggers +using .Agents + +using .AgentHandlers +using .StrategiesHandlers + +@get "/ping" function ping(req::HTTP.Request) + @info "Triggered endpoint: GET /ping" + return "pong" +end + + +function logging_middleware(handler) + return function(req::HTTP.Request) + t = time() + @info "Request: $(req.method) $(req.target)" + response = handler(req) + duration = round((time() - t) * 1000, digits=2) + @info "Response: $(response.status) ($(duration)ms)" + return response + end +end + + +# API Key Authentication Middleware +function auth_middleware(handler) + return function(req::HTTP.Request) + auth_enabled = MainAppConfig.get_value(APP_CONFIG, "security.enable_authentication", true) + @info "AuthMiddleware: auth_enabled = $auth_enabled" + + if !auth_enabled + return handler(req) # Authentication is disabled, proceed + end + + api_key_header = HTTP.header(req, "X-API-Key", "") + + if isempty(api_key_header) + @warn "AuthMiddleware: Missing X-API-Key header" + return HTTP.Response(401, ["Content-Type" => "application/json"], body=JSON3.write(Dict("error" => "Unauthorized: Missing API Key"))) + end + + valid_keys = MainAppConfig.get_value(APP_CONFIG, "security.api_keys", ["default-secret-key-please-change"]) + if !(valid_keys isa AbstractVector) + @error "AuthMiddleware: 'security.api_keys' in config is not a list. Denying access." + return HTTP.Response(500, ["Content-Type" => "application/json"], body=JSON3.write(Dict("error" => "Server configuration error"))) + end + + if !(api_key_header in valid_keys) + @warn "AuthMiddleware: Invalid API Key provided" + return HTTP.Response(403, ["Content-Type" => "application/json"], body=JSON3.write(Dict("error" => "Forbidden: Invalid API Key"))) + end + + return handler(req) + end +end + + +allowed_origins = [ "Access-Control-Allow-Origin" => "*" ] + +cors_headers = [ + allowed_origins..., + "Access-Control-Allow-Headers" => "*", + "Access-Control-Allow-Methods" => "GET, POST" +] + +function cors_middleware(handle) + return function (req::HTTP.Request) + # return headers on OPTIONS request + if HTTP.method(req) == "OPTIONS" + return HTTP.Response(200, cors_headers) + else + r = handle(req) + append!(r.headers, allowed_origins) + return r + end + end +end + +end # module RouterManager \ No newline at end of file diff --git a/backend/src/api/StrategiesHandlers.jl b/backend/src/api/StrategiesHandlers.jl new file mode 100644 index 00000000..f3e53c57 --- /dev/null +++ b/backend/src/api/StrategiesHandlers.jl @@ -0,0 +1,17 @@ +module StrategiesHandlers + +using HTTP + +using ..JuliaOSServer +using ..Agents: Agents, Triggers + +@registerStrategiesApi function list_strategies(req::HTTP.Request;)::Vector{StrategySummary} + @info "Triggered endpoint: GET /strategies" + strategies = Vector{StrategySummary}() + for (name, spec) in Agents.Strategies.STRATEGY_REGISTRY + push!(strategies, StrategySummary(name)) + end + return strategies +end + +end # module JuliaOSV1Server \ No newline at end of file diff --git a/backend/src/api/ToolHandlers.jl b/backend/src/api/ToolHandlers.jl new file mode 100644 index 00000000..a18d8938 --- /dev/null +++ b/backend/src/api/ToolHandlers.jl @@ -0,0 +1,17 @@ +module ToolsHandlers + +using HTTP + +using ..JuliaOSServer +using ..Agents: Agents, Triggers + +@registerToolApi function list_tools(req::HTTP.Request;)::Vector{ToolSummary} + @info "Triggered endpoint: GET /tools" + tools = Vector{ToolSummary}() + for (name, tool) in Agents.Tools.TOOL_REGISTRY + push!(tools, ToolSummary(name, ToolSummaryMetadata(tool.metadata.description))) + end + return tools +end + +end # module JuliaOSV1Server \ No newline at end of file diff --git a/backend/src/api/openapi_server_extensions.jl b/backend/src/api/openapi_server_extensions.jl deleted file mode 100644 index 97f1594b..00000000 --- a/backend/src/api/openapi_server_extensions.jl +++ /dev/null @@ -1,25 +0,0 @@ -using OpenAPI -using JSON -import OpenAPI.Servers: to_param_type - -""" - to_param_type(::Type{Dict{String,Any}}, body::AbstractString; stylectx=nothing) - -Parse an `application/json` body into a `Dict{String,Any}`. - -- Empty body → empty `Dict`. -- Invalid JSON → wrap the original error in an `ArgumentError` - so the generated 400-handling still works. -""" -function to_param_type(::Type{Dict{String,Any}}, - body::AbstractString; - stylectx = nothing) - - isempty(body) && return Dict{String,Any}() - - try - return JSON.parse(body) - catch err - throw(ArgumentError("invalid JSON body: $(sprint(showerror, err))")) - end -end diff --git a/backend/src/api/server/.openapi-generator/FILES b/backend/src/api/server/.openapi-generator/FILES index 209718e6..04ebfd17 100644 --- a/backend/src/api/server/.openapi-generator/FILES +++ b/backend/src/api/server/.openapi-generator/FILES @@ -1,17 +1,21 @@ README.md +docs/AgentApi.md docs/AgentBlueprint.md docs/AgentSummary.md docs/AgentUpdate.md docs/CreateAgentRequest.md -docs/DefaultApi.md +docs/StrategiesApi.md docs/StrategyBlueprint.md docs/StrategySummary.md +docs/ToolApi.md docs/ToolBlueprint.md docs/ToolSummary.md docs/ToolSummaryMetadata.md docs/TriggerConfig.md src/JuliaOSServer.jl -src/apis/api_DefaultApi.jl +src/apis/api_AgentApi.jl +src/apis/api_StrategiesApi.jl +src/apis/api_ToolApi.jl src/modelincludes.jl src/models/model_AgentBlueprint.jl src/models/model_AgentSummary.jl diff --git a/backend/src/api/server/.openapi-generator/VERSION b/backend/src/api/server/.openapi-generator/VERSION index 4bc5d618..5f84a81d 100644 --- a/backend/src/api/server/.openapi-generator/VERSION +++ b/backend/src/api/server/.openapi-generator/VERSION @@ -1 +1 @@ -7.9.0 +7.12.0 diff --git a/backend/src/api/server/src/JuliaOSServer.jl b/backend/src/api/server/src/JuliaOSServer.jl index 5b0860f1..02dba173 100644 --- a/backend/src/api/server/src/JuliaOSServer.jl +++ b/backend/src/api/server/src/JuliaOSServer.jl @@ -25,33 +25,187 @@ The following server methods must be implemented: - **list_agents** - *invocation:* GET /agents - *signature:* list_agents(req::HTTP.Request;) -> Vector{AgentSummary} -- **list_strategies** - - *invocation:* GET /strategies - - *signature:* list_strategies(req::HTTP.Request;) -> Vector{StrategySummary} -- **list_tools** - - *invocation:* GET /tools - - *signature:* list_tools(req::HTTP.Request;) -> Vector{ToolSummary} - **process_agent_webhook** - *invocation:* POST /agents/{agent_id}/webhook - *signature:* process_agent_webhook(req::HTTP.Request, agent_id::String; request_body=nothing,) -> Nothing - **update_agent** - *invocation:* PUT /agents/{agent_id} - *signature:* update_agent(req::HTTP.Request, agent_id::String, agent_update::AgentUpdate;) -> AgentSummary +- **list_strategies** + - *invocation:* GET /strategies + - *signature:* list_strategies(req::HTTP.Request;) -> Vector{StrategySummary} +- **list_tools** + - *invocation:* GET /tools + - *signature:* list_tools(req::HTTP.Request;) -> Vector{ToolSummary} """ module JuliaOSServer + +using JSON3 using HTTP using URIs -using Dates -using TimeZones using OpenAPI +using Oxygen using OpenAPI.Servers +using Dates +using TimeZones +using Oxygen +using Oxygen.Reflection +using StructTypes +using Oxygen.Errors +import Oxygen: format_response!, handlerequest const API_VERSION = "0.1.0" +Oxygen.Core.oxygen_title=""" + -== -- --- -- + =@@@@@@@@@. -@@@= =@@@= - - @@#- - #@@@@@@@@@@@# - + #@@# -@@@= @@@ @@@@- -- -=#=- =@@@@@@@###@@@@@@@= + #@@# -@@@= -.- - == ###=- - #@#- - =@@@@# - - #@@@@@ + #@@# -@@@= #@@@@ -- -@@@@= - -#@@ @@@@#- @@#= + #@@# =##=- =##= @@@= =## =#@@@@## =#= -=##=- ===- =#=- -=#= @@@@#- - + #@@# =@@#- #@@# @@@= #@@ #@@@@#==#@@@@# - - @@@@# - #@@@# #@@@@# - + #@@# =@@#- #@@# @@@= #@@ @@@=- =@@@ #@@@#- == -#@@# - ###= -=@@@@@@@##=== - + #@@# =@@#- #@@# @@@= #@@ ### #@@# @@@@@ - @@@@= - @@@- - =#@@@@@@@@@@@@# - + #@@# =@@#- #@@# @@@= #@@ =======#@@# ===--#@@@@= - -#@@@# - - ====#@@@@@@ + #@@# =@@#- #@@# @@@= #@@ =#@@@@@@@@@@@@@@# - =#= #@@@@=- == --#@@@= - - @@@@@ + #@@# .@@#- -#@@# @@@= #@@ =@@@= -------- @@# -=@@@= - =@@@@@- #@@@@ - = =@@@@= + @@@ @@@= =@@@# @@@= #@@ @@@ @@# -=@@# ###= #@@@=- ###= #@@=- #@@@@=- #@@@@ + -#@@@ @@@= -=@@@@# @@@= #@@ #@@#- -=@@@# -=- @@@@@@ - @@@@@= -- #@@@@@#= =#@@@@@= + - =#@@@@=. #@@@#= =#@@@@@@# @@@= #@@ -#@@@# -=@@@@@ -=@@@# #@@#=- ### - #@@@@@@@@@@@@@@@# + -@@@#=. #@@@@@@@# #@@= ##@= #@@ =#@@@@@@@= #@# --- @@@@@@ - ==########= - $(API_VERSION) + -- ----- ---- #@@# + -- +""" + +""" +Stores API interface specifications including HTTP metadata, function signature and return type +""" +struct APISpec + http_method::String + path::String + handler::Function + return_type::Type +end + +""" +Global registry for API specifications +Structure: Dict{Symbol => Vector{APISpec}} +""" +const INTERFACE_SPECS = Dict{Symbol, Vector{APISpec}}() + +""" +Macro for defining interface specifications +""" +macro interface(api_group, http_method, path, func_def) + # Extract function name and return type + func_expr = func_def.args[1] + + # Get function name and return type + if func_expr.head == :(::) # Function definition with return type + call_expr = func_expr.args[1] + func_name = call_expr.args[1] + return_type = func_expr.args[2] + else # Function definition without return type + func_name = func_expr.args[1] + return_type = :Any # Use Any as default return type + end + + # Convert function definition to actual function + func = eval(quote + $(func_def) + end) + + # Register interface specification + quote + if !haskey(INTERFACE_SPECS, $(esc(api_group))) + INTERFACE_SPECS[$(esc(api_group))] = [] + end + push!(INTERFACE_SPECS[$(esc(api_group))], + APISpec($http_method, $path, $func, $return_type)) + nothing + end +end + +""" +Global registry for generated handler functions +Structure: Dict{Symbol => Dict{Symbol => Dict{Symbol => Function}}} +- First level: API group name (e.g., :TestApi) +- Second level: Operation name (e.g., :test_hello) +- Third level: Handler function type (:read, :validate, :invoke) +""" +const GENERATED_HANDLERS = Dict{Symbol, Dict{Symbol, Dict{Symbol, Function}}}() + + +""" +Parameters: +- api_group::Symbol: API group name (e.g., :TestApi) +- operation_name::Symbol: Operation name (e.g., :test_hello) +- handler_type::Symbol: Handler function type (:read, :validate, :invoke) +- handler_func::Function: Handler function +""" +macro generated_handler(api_group, op_name, h_type, handler_func) + # Convert function definition to actual function + func = eval(quote + $(handler_func) + end) + + quote + @assert $(esc(h_type)) in (:read, :validate, :invoke) "Handler type must be :read, :validate or :invoke" + if !haskey(GENERATED_HANDLERS, $(esc(api_group))) + GENERATED_HANDLERS[$(esc(api_group))] = Dict{Symbol, Dict{Symbol, Function}}() + end + if !haskey(GENERATED_HANDLERS[$(esc(api_group))], $(esc(op_name))) + GENERATED_HANDLERS[$(esc(api_group))][$(esc(op_name))] = Dict{Symbol, Function}() + end + GENERATED_HANDLERS[$(esc(api_group))][$(esc(op_name))][$(esc(h_type))] = $func + nothing + end +end + +""" +Get all handler functions for a specific operation + +Parameters: +- api_group::Symbol: API group name +- operation_name::Symbol: Operation name + +Returns: +- NamedTuple: (read=Function, validate=Function, invoke=Function) or nothing +""" +function get_operation_handlers(api_group::Symbol, operation_name::Symbol) + if !haskey(GENERATED_HANDLERS, api_group) + @warn "API group not found: $api_group" + return nothing + end + + if !haskey(GENERATED_HANDLERS[api_group], operation_name) + @warn "Operation not found: $api_group.$operation_name" + return nothing + end + + handlers = GENERATED_HANDLERS[api_group][operation_name] + + # Check if all required handler functions are present + required_types = [:read, :validate, :invoke] + for handler_type in required_types + if !haskey(handlers, handler_type) + @warn "Missing handler function: $api_group.$operation_name.$handler_type" + return nothing + end + end + + return ( + read = handlers[:read], + validate = handlers[:validate], + invoke = handlers[:invoke] + ) +end include("modelincludes.jl") -include("apis/api_DefaultApi.jl") +include("apis/api_AgentApi.jl") +include("apis/api_StrategiesApi.jl") +include("apis/api_ToolApi.jl") """ Register handlers for all APIs in this module in the supplied `Router` instance. @@ -74,10 +228,446 @@ The order in which middlewares are invoked are: `init |> read |> pre_validation |> validate |> pre_invoke |> invoke |> post_invoke` """ function register(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - registerDefaultApi(router, impl; path_prefix=path_prefix, optional_middlewares...) + registerAgentApi(router, impl; path_prefix=path_prefix, optional_middlewares...) + registerStrategiesApi(router, impl; path_prefix=path_prefix, optional_middlewares...) + registerToolApi(router, impl; path_prefix=path_prefix, optional_middlewares...) return router end + +""" +Automatically build OpenAPI middleware chain. + +Arguments: +- api_group::Symbol: Name of the API group +- operation_name::Symbol: Name of the operation +- impl: Implementation object +- optional_middlewares...: Optional extra middlewares + +Returns: +- Function: Composed middleware function +""" +function build_openapi_middleware(api_group::Symbol, operation_name::Symbol, impl; optional_middlewares...) + handlers = get_operation_handlers(api_group, operation_name) + + if isnothing(handlers) + error("Failed to build middleware chain: missing necessary handler for $api_group.$operation_name") + end + + @info "🔧 Building middleware chain: $api_group.$operation_name" + @info " - Read function: $(nameof(handlers.read))" + @info " - Validate function: $(nameof(handlers.validate))" + @info " - Invoke function: $(nameof(handlers.invoke))" + + # Build the middleware chain using OpenAPI.Servers.middleware + return OpenAPI.Servers.middleware( + impl, + handlers.read, + handlers.validate, + handlers.invoke; + optional_middlewares... + ) +end + + +""" +Check whether two types are compatible. +""" +function is_type_compatible(required_type::Type, actual_type::Type) + # If required type is Any, all types are compatible + required_type === Any && return true + + # If actual type is a subtype of required type, it's compatible + actual_type <: required_type && return true + + return false +end + +""" +Stores the result of function implementation check. +""" +struct ImplementationCheckResult + success::Bool # Whether the check passed + matching_spec::Union{APISpec, Nothing} # Matched spec + error_message::Union{String, Nothing} # Error message (if any) + impl_func::Function # Implementation function + impl_return_type::Type # Return type of implementation +end + +""" +Check whether a single function implementation conforms to spec. + +Returns: +- ImplementationCheckResult: Detailed check result +""" +function check_single_implementation(api_group::Symbol, impl_func::Function, impl_return_type::Type) + if !haskey(INTERFACE_SPECS, api_group) + return ImplementationCheckResult( + false, + nothing, + "No API spec found for group $api_group", + impl_func, + impl_return_type + ) + end + + specs = INTERFACE_SPECS[api_group] + matching_spec = nothing + + # Find matching spec + for spec in specs + if nameof(spec.handler) == nameof(impl_func) + matching_spec = spec + break + end + end + + if isnothing(matching_spec) + return ImplementationCheckResult( + false, + nothing, + "No API spec definition found for function $(nameof(impl_func))", + impl_func, + impl_return_type + ) + end + + # Validate parameters + @info "🔍 Validating parameters..." + spec_func_details = Oxygen.Core.parse_func_params(matching_spec.path, matching_spec.handler) + handler_func_details = Oxygen.Core.parse_func_params(matching_spec.path, impl_func) + + # Only check basic structure + if handler_func_details.info.name != spec_func_details.info.name + return ImplementationCheckResult( + false, + matching_spec, + "Function name mismatch", + impl_func, + impl_return_type + ) + end + + if length(handler_func_details.info.sig) != length(spec_func_details.info.sig) + return ImplementationCheckResult( + false, + matching_spec, + "Parameter count mismatch", + impl_func, + impl_return_type + ) + end + + # Check return type + spec_return_type = matching_spec.return_type + if !is_type_compatible(spec_return_type, impl_return_type) + return ImplementationCheckResult( + false, + matching_spec, + "Incompatible return type: expected $(spec_return_type), got $(impl_return_type)", + impl_func, + impl_return_type + ) + end + + return ImplementationCheckResult( + true, + matching_spec, + nothing, + impl_func, + impl_return_type + ) +end + +""" +Parse function definition and extract name and return type. + +Arguments: +- func_def: Function definition expression + +Returns: +- (func_name, return_type): Tuple containing function name and return type +""" +function parse_function_definition(func_def) + func_expr = func_def.args[1] + + # Extract function name and return type + if func_expr.head == :(::) # Function with return type annotation + call_expr = func_expr.args[1] + func_name = call_expr.args[1] + return_type = func_expr.args[2] + else # Function without return type + func_name = func_expr.args[1] + return_type = :Any # Default to Any + end + + return func_name, return_type +end + +""" +Parse registration configuration parameters. +""" +function parse_register_config(config) + default_config = ( + prefix = "", + tags = String[], + middleware = Function[], + interval = nothing, + cron = nothing + ) + + if config isa String || config isa Symbol + return merge(default_config, (prefix = config,)) + elseif Meta.isexpr(config, :tuple) + return merge( + default_config, + ( + prefix = config.args[1], + tags = length(config.args) > 1 ? config.args[2] : String[], + middleware = length(config.args) > 2 ? config.args[3] : Function[], + interval = length(config.args) > 3 ? config.args[4] : nothing, + cron = length(config.args) > 4 ? config.args[5] : nothing + ) + ) + else + error("Invalid config parameter format") + end +end + +""" +Serialize handler with error formatting and HTTP response wrapping. +""" +function serializer_handler(handler) + function serializer_with_http_response_handler(req::HTTP.Request) + return handlerequest(true; show_errors=true) do + try + format_response!(req, handler(req)) # Format and save response + return req.response + catch e + if isa(e, OpenAPI.ValidationException) + return HTTP.Response(422, + ["Content-Type" => "application/json"], + body=JSON3.write(Dict( + "error" => "Unprocessable Entity", + "message" => e.reason, + "details" => Dict( + "loc" => e.parameter, + "msg" => e.reason, + "type" => e.rule, + "input" => e.value + ) + )) + ) + elseif isa(e, Oxygen.Errors.ValidationError) + return HTTP.Response(422, + ["Content-Type" => "application/json"], + body=JSON3.write(Dict( + "error" => "Unprocessable Entity", + "message" => e.msg, + "details" => e.cause + )) + ) + end + rethrow(e) + end + end + end +end + +""" +Register API handler to routing system + +Parameters: +- api_group::Symbol: API group name +- func::Function: handler function +- return_type::Type: return type of the function +- prefix::String: API path prefix + +Returns: +- Bool: whether registration was successful +""" +function register_api_route( + api_group::Symbol, + func, + return_type::Type; + prefix::String="", + tags::Vector{String}=Vector{String}(), + middleware::Union{Vector, Nothing}=nothing, + interval::Union{Real, Nothing}=nothing, + cron::Union{String, Nothing}=nothing + ) + @info "🚀 Starting API route registration: $api_group.$(nameof(func))" + + # Ensure prefix format is correct + prefix = normalize_path_prefix(prefix) + + # Ensure API group exists + if !haskey(INTERFACE_SPECS, api_group) + @error "❌ API group $api_group not found in interface specification" + error("API group $api_group not defined") + return false + end + + # Check implementation + result = check_single_implementation(api_group, func, return_type) + if !result.success + @error "❌ Implementation check failed: $(result.error_message)" + error("Function $(string(nameof(func))) does not conform to spec: $(result.error_message)") + return false + end + @info "✅ Function $(string(nameof(func))) passed implementation check" + + # Register route + if !isnothing(result.matching_spec) + route = prefix * result.matching_spec.path + httpmethod = result.matching_spec.http_method + operation_name = nameof(result.matching_spec.handler) + @info "🌐 Registering route: $httpmethod $route" + try + + actual_middleware = isnothing(middleware) ? Function[] : middleware + # TODO + # openapi_middleware = build_openapi_middleware(api_group, operation_name, func; actual_middleware...) + parsed_route = Oxygen.Core.parse_route(httpmethod, route) + func_details = Oxygen.Core.parse_func_params(route, func) + + # Generate OpenAPI documentation (if enabled) + if Oxygen.CONTEXT[].docs.enabled[] + generate_openapi_docs(parsed_route, httpmethod, func_details, return_type) + end + + # Register handler + Oxygen.Core.registerhandler( + Oxygen.CONTEXT[], + Oxygen.CONTEXT[].service.router, + httpmethod, + parsed_route, + func, #TODO + func_details + ) + + @info "✨ Route registered successfully: $httpmethod $route" + return true + catch e + @error "❌ Route registration failed" exception=(e, catch_backtrace()) + return false + end + end + return false +end + +""" +Normalize path prefix +""" +function normalize_path_prefix(prefix::String) + # Ensure prefix starts with a slash + if !startswith(prefix, "/") + prefix = "/" * prefix + end + + # Ensure prefix does not end with a slash + if endswith(prefix, "/") + prefix = prefix[1:end-1] + end + + return prefix +end + +""" +Generate OpenAPI documentation +""" +function generate_openapi_docs(route, httpmethod, func_details, return_type) + try + queryparams = func_details.queryparams + pathparams = func_details.pathparams + headers = func_details.headers + bodyparams = func_details.bodyargs + + @info "📚 Generating OpenAPI docs: $route" + + Oxygen.Core.registerschema( + Oxygen.CONTEXT[].docs, + route, + httpmethod, + pathparams, + queryparams, + headers, + bodyparams, + Vector([return_type]) + ) + catch error + @warn "Failed to generate OpenAPI docs: $route - $error" + end +end + +# Define forced parameters (not user-overridable) +const FORCED_PARAMS = Dict( + :serialize => false, # Force disable default serializer since we use custom serializer_handler +) + +""" + serve([ctx::ServerContext]; + middleware::Vector=[], + host::String="127.0.0.1", + port::Int=8080; + kwargs...) + +Wrapper for Oxygen.serve to ensure correct middleware order and serialization handling. + +Parameters: +- ctx: optional ServerContext +- middleware: user-defined middleware list +- host: server host +- port: server port +- kwargs: additional parameters passed to Oxygen.serve + +Config options: +- async::Bool=false: run server asynchronously +- parallel::Bool=false: process requests in parallel +- catch_errors::Bool=true: catch errors +- show_errors::Bool=true: show error details +- docs::Bool=true: enable API documentation +- metrics::Bool=true: enable metrics collection +- show_banner::Bool=true: show startup banner +- docs_path::String="/docs": API docs path +- schema_path::String="/schema": OpenAPI schema path +- external_url::Union{String,Nothing}=nothing: external access URL + +Returns: +- Oxygen.Server instance +""" +function serve(; + host::String="127.0.0.1", + port::Int=8080, + middleware::Vector=[], + kwargs...) + async = Base.get(kwargs, :async, false) + + try + # Ensure serializer_handler is always at the end of middleware chain + final_middleware = if isempty(middleware) + [serializer_handler] + else + [middleware..., serializer_handler] + end + merged_params = merge(FORCED_PARAMS, Dict(kwargs)) + return Oxygen.Core.serve( + Oxygen.CONTEXT[]; + middleware=final_middleware, + host=host, + port=port, + merged_params...) + finally + # Close server on exit if not running asynchronously + if !async + terminate() + # Only reset state on exit if running interactively + isinteractive() && resetstate() + end + end +end + + # export models export AgentBlueprint export AgentSummary @@ -90,4 +680,5 @@ export ToolSummary export ToolSummaryMetadata export TriggerConfig +export @interface,@generated_handler,serve end # module JuliaOSServer diff --git a/backend/src/api/server/src/apis/api_AgentApi.jl b/backend/src/api/server/src/apis/api_AgentApi.jl new file mode 100644 index 00000000..094c43a1 --- /dev/null +++ b/backend/src/api/server/src/apis/api_AgentApi.jl @@ -0,0 +1,479 @@ +# This file was generated by the Julia OpenAPI Code Generator +# Do not modify this file directly. Modify the OpenAPI specification instead. + + +@interface :AgentApi "POST" "/agents" function create_agent(req::HTTP.Request, create_agent_request::CreateAgentRequest;)::AgentSummary + error("Method not implemented") +end +@interface :AgentApi "DELETE" "/agents/{agent_id}" function delete_agent(req::HTTP.Request, agent_id::String;)::Any + error("Method not implemented") +end +@interface :AgentApi "GET" "/agents/{agent_id}" function get_agent(req::HTTP.Request, agent_id::String;)::AgentSummary + error("Method not implemented") +end +@interface :AgentApi "GET" "/agents/{agent_id}/logs" function get_agent_logs(req::HTTP.Request, agent_id::String;)::Dict{String, Any} + error("Method not implemented") +end +@interface :AgentApi "GET" "/agents/{agent_id}/output" function get_agent_output(req::HTTP.Request, agent_id::String;)::Dict{String, Any} + error("Method not implemented") +end +@interface :AgentApi "GET" "/agents" function list_agents(req::HTTP.Request;)::Vector{AgentSummary} + error("Method not implemented") +end +@interface :AgentApi "POST" "/agents/{agent_id}/webhook" function process_agent_webhook(req::HTTP.Request, agent_id::String; request_body=nothing,)::Any + error("Method not implemented") +end +@interface :AgentApi "PUT" "/agents/{agent_id}" function update_agent(req::HTTP.Request, agent_id::String, agent_update::AgentUpdate;)::AgentSummary + error("Method not implemented") +end + +@generated_handler :AgentApi :create_agent :read function create_agent_read(handler) + function create_agent_read_handler(req::HTTP.Request, create_agent_request::CreateAgentRequest;)::AgentSummary + openapi_params = Dict{String,Any}() + openapi_params["CreateAgentRequest"] = OpenAPI.Servers.to_param_type(CreateAgentRequest, String(req.body)) + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +@generated_handler :AgentApi :create_agent :validate function create_agent_validate(handler) + function create_agent_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + op = "create_agent" + + n = "CreateAgentRequest" + v = get(openapi_params, n, nothing) + isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) + if !isnothing(v) + if isa(v, OpenAPI.APIModel) + OpenAPI.validate_properties(v) + if !OpenAPI.check_required(v) + throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) + end + end + end + + return handler(req) + end +end + +@generated_handler :AgentApi :create_agent :invoke function create_agent_invoke(impl; post_invoke=nothing) + function create_agent_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.create_agent(req::HTTP.Request, openapi_params["CreateAgentRequest"];) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + +@generated_handler :AgentApi :delete_agent :read function delete_agent_read(handler) + function delete_agent_read_handler(req::HTTP.Request, agent_id::String;)::Any + openapi_params = Dict{String,Any}() + path_params = HTTP.getparams(req) + openapi_params["agent_id"] = OpenAPI.Servers.to_param(String, path_params, "agent_id", required=true, ) + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +@generated_handler :AgentApi :delete_agent :validate function delete_agent_validate(handler) + function delete_agent_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + op = "delete_agent" + + n = "agent_id" + v = get(openapi_params, n, nothing) + isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) + if !isnothing(v) + if isa(v, OpenAPI.APIModel) + OpenAPI.validate_properties(v) + if !OpenAPI.check_required(v) + throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) + end + end + end + + return handler(req) + end +end + +@generated_handler :AgentApi :delete_agent :invoke function delete_agent_invoke(impl; post_invoke=nothing) + function delete_agent_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.delete_agent(req::HTTP.Request, openapi_params["agent_id"];) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + +@generated_handler :AgentApi :get_agent :read function get_agent_read(handler) + function get_agent_read_handler(req::HTTP.Request, agent_id::String;)::AgentSummary + openapi_params = Dict{String,Any}() + path_params = HTTP.getparams(req) + openapi_params["agent_id"] = OpenAPI.Servers.to_param(String, path_params, "agent_id", required=true, ) + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +@generated_handler :AgentApi :get_agent :validate function get_agent_validate(handler) + function get_agent_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + op = "get_agent" + + n = "agent_id" + v = get(openapi_params, n, nothing) + isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) + if !isnothing(v) + if isa(v, OpenAPI.APIModel) + OpenAPI.validate_properties(v) + if !OpenAPI.check_required(v) + throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) + end + end + end + + return handler(req) + end +end + +@generated_handler :AgentApi :get_agent :invoke function get_agent_invoke(impl; post_invoke=nothing) + function get_agent_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.get_agent(req::HTTP.Request, openapi_params["agent_id"];) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + +@generated_handler :AgentApi :get_agent_logs :read function get_agent_logs_read(handler) + function get_agent_logs_read_handler(req::HTTP.Request, agent_id::String;)::Dict{String, Any} + openapi_params = Dict{String,Any}() + path_params = HTTP.getparams(req) + openapi_params["agent_id"] = OpenAPI.Servers.to_param(String, path_params, "agent_id", required=true, ) + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +@generated_handler :AgentApi :get_agent_logs :validate function get_agent_logs_validate(handler) + function get_agent_logs_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + op = "get_agent_logs" + + n = "agent_id" + v = get(openapi_params, n, nothing) + isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) + if !isnothing(v) + if isa(v, OpenAPI.APIModel) + OpenAPI.validate_properties(v) + if !OpenAPI.check_required(v) + throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) + end + end + end + + return handler(req) + end +end + +@generated_handler :AgentApi :get_agent_logs :invoke function get_agent_logs_invoke(impl; post_invoke=nothing) + function get_agent_logs_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.get_agent_logs(req::HTTP.Request, openapi_params["agent_id"];) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + +@generated_handler :AgentApi :get_agent_output :read function get_agent_output_read(handler) + function get_agent_output_read_handler(req::HTTP.Request, agent_id::String;)::Dict{String, Any} + openapi_params = Dict{String,Any}() + path_params = HTTP.getparams(req) + openapi_params["agent_id"] = OpenAPI.Servers.to_param(String, path_params, "agent_id", required=true, ) + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +@generated_handler :AgentApi :get_agent_output :validate function get_agent_output_validate(handler) + function get_agent_output_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + op = "get_agent_output" + + n = "agent_id" + v = get(openapi_params, n, nothing) + isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) + if !isnothing(v) + if isa(v, OpenAPI.APIModel) + OpenAPI.validate_properties(v) + if !OpenAPI.check_required(v) + throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) + end + end + end + + return handler(req) + end +end + +@generated_handler :AgentApi :get_agent_output :invoke function get_agent_output_invoke(impl; post_invoke=nothing) + function get_agent_output_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.get_agent_output(req::HTTP.Request, openapi_params["agent_id"];) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + +@generated_handler :AgentApi :list_agents :read function list_agents_read(handler) + function list_agents_read_handler(req::HTTP.Request;)::Vector{AgentSummary} + openapi_params = Dict{String,Any}() + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +@generated_handler :AgentApi :list_agents :validate function list_agents_validate(handler) + function list_agents_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + op = "list_agents" + + return handler(req) + end +end + +@generated_handler :AgentApi :list_agents :invoke function list_agents_invoke(impl; post_invoke=nothing) + function list_agents_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.list_agents(req::HTTP.Request;) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + +@generated_handler :AgentApi :process_agent_webhook :read function process_agent_webhook_read(handler) + function process_agent_webhook_read_handler(req::HTTP.Request, agent_id::String; request_body=nothing,)::Any + openapi_params = Dict{String,Any}() + path_params = HTTP.getparams(req) + openapi_params["agent_id"] = OpenAPI.Servers.to_param(String, path_params, "agent_id", required=true, ) + openapi_params["request_body"] = OpenAPI.Servers.to_param_type(Dict{String, Any}, String(req.body)) + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +@generated_handler :AgentApi :process_agent_webhook :validate function process_agent_webhook_validate(handler) + function process_agent_webhook_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + op = "process_agent_webhook" + + n = "agent_id" + v = get(openapi_params, n, nothing) + isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) + if !isnothing(v) + if isa(v, OpenAPI.APIModel) + OpenAPI.validate_properties(v) + if !OpenAPI.check_required(v) + throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) + end + end + end + + n = "request_body" + v = get(openapi_params, n, nothing) + if !isnothing(v) + if isa(v, OpenAPI.APIModel) + OpenAPI.validate_properties(v) + if !OpenAPI.check_required(v) + throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) + end + end + end + + return handler(req) + end +end + +@generated_handler :AgentApi :process_agent_webhook :invoke function process_agent_webhook_invoke(impl; post_invoke=nothing) + function process_agent_webhook_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.process_agent_webhook(req::HTTP.Request, openapi_params["agent_id"]; request_body=get(openapi_params, "request_body", nothing),) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + +@generated_handler :AgentApi :update_agent :read function update_agent_read(handler) + function update_agent_read_handler(req::HTTP.Request, agent_id::String, agent_update::AgentUpdate;)::AgentSummary + openapi_params = Dict{String,Any}() + path_params = HTTP.getparams(req) + openapi_params["agent_id"] = OpenAPI.Servers.to_param(String, path_params, "agent_id", required=true, ) + openapi_params["AgentUpdate"] = OpenAPI.Servers.to_param_type(AgentUpdate, String(req.body)) + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +@generated_handler :AgentApi :update_agent :validate function update_agent_validate(handler) + function update_agent_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + op = "update_agent" + + n = "agent_id" + v = get(openapi_params, n, nothing) + isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) + if !isnothing(v) + if isa(v, OpenAPI.APIModel) + OpenAPI.validate_properties(v) + if !OpenAPI.check_required(v) + throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) + end + end + end + + n = "AgentUpdate" + v = get(openapi_params, n, nothing) + isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)) + if !isnothing(v) + if isa(v, OpenAPI.APIModel) + OpenAPI.validate_properties(v) + if !OpenAPI.check_required(v) + throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) + end + end + end + + return handler(req) + end +end + +@generated_handler :AgentApi :update_agent :invoke function update_agent_invoke(impl; post_invoke=nothing) + function update_agent_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.update_agent(req::HTTP.Request, openapi_params["agent_id"], openapi_params["AgentUpdate"];) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + + +function registerAgentApi(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) + HTTP.register!(router, "POST", path_prefix * "/agents", OpenAPI.Servers.middleware(impl, create_agent_read, create_agent_validate, create_agent_invoke; optional_middlewares...)) + HTTP.register!(router, "DELETE", path_prefix * "/agents/{agent_id}", OpenAPI.Servers.middleware(impl, delete_agent_read, delete_agent_validate, delete_agent_invoke; optional_middlewares...)) + HTTP.register!(router, "GET", path_prefix * "/agents/{agent_id}", OpenAPI.Servers.middleware(impl, get_agent_read, get_agent_validate, get_agent_invoke; optional_middlewares...)) + HTTP.register!(router, "GET", path_prefix * "/agents/{agent_id}/logs", OpenAPI.Servers.middleware(impl, get_agent_logs_read, get_agent_logs_validate, get_agent_logs_invoke; optional_middlewares...)) + HTTP.register!(router, "GET", path_prefix * "/agents/{agent_id}/output", OpenAPI.Servers.middleware(impl, get_agent_output_read, get_agent_output_validate, get_agent_output_invoke; optional_middlewares...)) + HTTP.register!(router, "GET", path_prefix * "/agents", OpenAPI.Servers.middleware(impl, list_agents_read, list_agents_validate, list_agents_invoke; optional_middlewares...)) + HTTP.register!(router, "POST", path_prefix * "/agents/{agent_id}/webhook", OpenAPI.Servers.middleware(impl, process_agent_webhook_read, process_agent_webhook_validate, process_agent_webhook_invoke; optional_middlewares...)) + HTTP.register!(router, "PUT", path_prefix * "/agents/{agent_id}", OpenAPI.Servers.middleware(impl, update_agent_read, update_agent_validate, update_agent_invoke; optional_middlewares...)) + return router +end + + +""" +Macro to register a API route. + +Usage examples: +@registerAgentApi ("/api/v1",["AgentApi"]) function create_agent(req::HTTP.Request, create_agent_request::CreateAgentRequest;)::AgentSummary + # ... +end +@registerAgentApi ("/api/v1",["AgentApi"]) function delete_agent(req::HTTP.Request, agent_id::String;)::Any + # ... +end +@registerAgentApi ("/api/v1",["AgentApi"]) function get_agent(req::HTTP.Request, agent_id::String;)::AgentSummary + # ... +end +@registerAgentApi ("/api/v1",["AgentApi"]) function get_agent_logs(req::HTTP.Request, agent_id::String;)::Dict{String, Any} + # ... +end +@registerAgentApi ("/api/v1",["AgentApi"]) function get_agent_output(req::HTTP.Request, agent_id::String;)::Dict{String, Any} + # ... +end +@registerAgentApi ("/api/v1",["AgentApi"]) function list_agents(req::HTTP.Request;)::Vector{AgentSummary} + # ... +end +@registerAgentApi ("/api/v1",["AgentApi"]) function process_agent_webhook(req::HTTP.Request, agent_id::String; request_body=nothing,)::Any + # ... +end +@registerAgentApi ("/api/v1",["AgentApi"]) function update_agent(req::HTTP.Request, agent_id::String, agent_update::AgentUpdate;)::AgentSummary + # ... +end +""" +macro registerAgentApi(config, func_def) + # Ensure the second argument is a function definition + if !Meta.isexpr(func_def, :function) + error("The second argument must be a function definition") + end + + # Handle the config parameter based on its type + if config isa String || config isa Symbol + # Simple case: only a route prefix + prefix = config + tags = String["AgentApi"] + middleware = nothing + interval = nothing + cron = nothing + elseif Meta.isexpr(config, :tuple) + # Tuple form with multiple configuration parameters + prefix = config.args[1] + tags = length(config.args) > 1 ? config.args[2] : String["AgentApi"] + middleware = length(config.args) > 2 ? config.args[3] : nothing + interval = length(config.args) > 3 ? config.args[4] : nothing + cron = length(config.args) > 4 ? config.args[5] : nothing + else + error("Invalid configuration parameter format") + end + + func_name, return_type = parse_function_definition(func_def) + + return quote + local result = $(esc(func_def)) + register_api_route( + :AgentApi, + result, + $(esc(return_type)); + prefix=$(esc(prefix)), + tags=$(esc(tags)), + middleware=$(middleware), + interval=$(esc(interval)), + cron=$(esc(cron)) + ) + result + end +end + +macro registerAgentApi(func_def) + if !Meta.isexpr(func_def, :function) + error("The argument must be a function definition") + end + + func_name, return_type = parse_function_definition(func_def) + + return quote + local result = $(esc(func_def)) + register_api_route( + :AgentApi, + result, + $(esc(return_type)); + prefix="/api/v1", + tags=String["AgentApi"], + middleware=nothing, + interval=nothing, + cron=nothing + ) + result + end +end + + +export @registerAgentApi + diff --git a/backend/src/api/server/src/apis/api_DefaultApi.jl b/backend/src/api/server/src/apis/api_DefaultApi.jl deleted file mode 100644 index 918c3454..00000000 --- a/backend/src/api/server/src/apis/api_DefaultApi.jl +++ /dev/null @@ -1,293 +0,0 @@ -# This file was generated by the Julia OpenAPI Code Generator -# Do not modify this file directly. Modify the OpenAPI specification instead. - - -function create_agent_read(handler) - function create_agent_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - openapi_params["CreateAgentRequest"] = OpenAPI.Servers.to_param_type(CreateAgentRequest, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function create_agent_validate(handler) - function create_agent_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - return handler(req) - end -end - -function create_agent_invoke(impl; post_invoke=nothing) - function create_agent_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.create_agent(req::HTTP.Request, openapi_params["CreateAgentRequest"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function delete_agent_read(handler) - function delete_agent_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["agent_id"] = OpenAPI.Servers.to_param(String, path_params, "agent_id", required=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function delete_agent_validate(handler) - function delete_agent_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - return handler(req) - end -end - -function delete_agent_invoke(impl; post_invoke=nothing) - function delete_agent_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.delete_agent(req::HTTP.Request, openapi_params["agent_id"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function get_agent_read(handler) - function get_agent_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["agent_id"] = OpenAPI.Servers.to_param(String, path_params, "agent_id", required=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function get_agent_validate(handler) - function get_agent_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - return handler(req) - end -end - -function get_agent_invoke(impl; post_invoke=nothing) - function get_agent_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.get_agent(req::HTTP.Request, openapi_params["agent_id"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function get_agent_logs_read(handler) - function get_agent_logs_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["agent_id"] = OpenAPI.Servers.to_param(String, path_params, "agent_id", required=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function get_agent_logs_validate(handler) - function get_agent_logs_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - return handler(req) - end -end - -function get_agent_logs_invoke(impl; post_invoke=nothing) - function get_agent_logs_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.get_agent_logs(req::HTTP.Request, openapi_params["agent_id"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function get_agent_output_read(handler) - function get_agent_output_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["agent_id"] = OpenAPI.Servers.to_param(String, path_params, "agent_id", required=true, ) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function get_agent_output_validate(handler) - function get_agent_output_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - return handler(req) - end -end - -function get_agent_output_invoke(impl; post_invoke=nothing) - function get_agent_output_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.get_agent_output(req::HTTP.Request, openapi_params["agent_id"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function list_agents_read(handler) - function list_agents_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function list_agents_validate(handler) - function list_agents_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - return handler(req) - end -end - -function list_agents_invoke(impl; post_invoke=nothing) - function list_agents_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.list_agents(req::HTTP.Request;) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function list_strategies_read(handler) - function list_strategies_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function list_strategies_validate(handler) - function list_strategies_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - return handler(req) - end -end - -function list_strategies_invoke(impl; post_invoke=nothing) - function list_strategies_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.list_strategies(req::HTTP.Request;) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function list_tools_read(handler) - function list_tools_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function list_tools_validate(handler) - function list_tools_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - return handler(req) - end -end - -function list_tools_invoke(impl; post_invoke=nothing) - function list_tools_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.list_tools(req::HTTP.Request;) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function process_agent_webhook_read(handler) - function process_agent_webhook_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["agent_id"] = OpenAPI.Servers.to_param(String, path_params, "agent_id", required=true, ) - openapi_params["request_body"] = OpenAPI.Servers.to_param_type(Dict{String, Any}, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function process_agent_webhook_validate(handler) - function process_agent_webhook_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - return handler(req) - end -end - -function process_agent_webhook_invoke(impl; post_invoke=nothing) - function process_agent_webhook_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.process_agent_webhook(req::HTTP.Request, openapi_params["agent_id"]; request_body=get(openapi_params, "request_body", nothing),) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - -function update_agent_read(handler) - function update_agent_read_handler(req::HTTP.Request) - openapi_params = Dict{String,Any}() - path_params = HTTP.getparams(req) - openapi_params["agent_id"] = OpenAPI.Servers.to_param(String, path_params, "agent_id", required=true, ) - openapi_params["AgentUpdate"] = OpenAPI.Servers.to_param_type(AgentUpdate, String(req.body)) - req.context[:openapi_params] = openapi_params - - return handler(req) - end -end - -function update_agent_validate(handler) - function update_agent_validate_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - - return handler(req) - end -end - -function update_agent_invoke(impl; post_invoke=nothing) - function update_agent_invoke_handler(req::HTTP.Request) - openapi_params = req.context[:openapi_params] - ret = impl.update_agent(req::HTTP.Request, openapi_params["agent_id"], openapi_params["AgentUpdate"];) - resp = OpenAPI.Servers.server_response(ret) - return (post_invoke === nothing) ? resp : post_invoke(req, resp) - end -end - - -function registerDefaultApi(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) - HTTP.register!(router, "POST", path_prefix * "/agents", OpenAPI.Servers.middleware(impl, create_agent_read, create_agent_validate, create_agent_invoke; optional_middlewares...)) - HTTP.register!(router, "DELETE", path_prefix * "/agents/{agent_id}", OpenAPI.Servers.middleware(impl, delete_agent_read, delete_agent_validate, delete_agent_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/agents/{agent_id}", OpenAPI.Servers.middleware(impl, get_agent_read, get_agent_validate, get_agent_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/agents/{agent_id}/logs", OpenAPI.Servers.middleware(impl, get_agent_logs_read, get_agent_logs_validate, get_agent_logs_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/agents/{agent_id}/output", OpenAPI.Servers.middleware(impl, get_agent_output_read, get_agent_output_validate, get_agent_output_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/agents", OpenAPI.Servers.middleware(impl, list_agents_read, list_agents_validate, list_agents_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/strategies", OpenAPI.Servers.middleware(impl, list_strategies_read, list_strategies_validate, list_strategies_invoke; optional_middlewares...)) - HTTP.register!(router, "GET", path_prefix * "/tools", OpenAPI.Servers.middleware(impl, list_tools_read, list_tools_validate, list_tools_invoke; optional_middlewares...)) - HTTP.register!(router, "POST", path_prefix * "/agents/{agent_id}/webhook", OpenAPI.Servers.middleware(impl, process_agent_webhook_read, process_agent_webhook_validate, process_agent_webhook_invoke; optional_middlewares...)) - HTTP.register!(router, "PUT", path_prefix * "/agents/{agent_id}", OpenAPI.Servers.middleware(impl, update_agent_read, update_agent_validate, update_agent_invoke; optional_middlewares...)) - return router -end diff --git a/backend/src/api/server/src/apis/api_StrategiesApi.jl b/backend/src/api/server/src/apis/api_StrategiesApi.jl new file mode 100644 index 00000000..cf836572 --- /dev/null +++ b/backend/src/api/server/src/apis/api_StrategiesApi.jl @@ -0,0 +1,119 @@ +# This file was generated by the Julia OpenAPI Code Generator +# Do not modify this file directly. Modify the OpenAPI specification instead. + + +@interface :StrategiesApi "GET" "/strategies" function list_strategies(req::HTTP.Request;)::Vector{StrategySummary} + error("Method not implemented") +end + +@generated_handler :StrategiesApi :list_strategies :read function list_strategies_read(handler) + function list_strategies_read_handler(req::HTTP.Request;)::Vector{StrategySummary} + openapi_params = Dict{String,Any}() + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +@generated_handler :StrategiesApi :list_strategies :validate function list_strategies_validate(handler) + function list_strategies_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + op = "list_strategies" + + return handler(req) + end +end + +@generated_handler :StrategiesApi :list_strategies :invoke function list_strategies_invoke(impl; post_invoke=nothing) + function list_strategies_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.list_strategies(req::HTTP.Request;) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + + +function registerStrategiesApi(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) + HTTP.register!(router, "GET", path_prefix * "/strategies", OpenAPI.Servers.middleware(impl, list_strategies_read, list_strategies_validate, list_strategies_invoke; optional_middlewares...)) + return router +end + + +""" +Macro to register a API route. + +Usage examples: +@registerStrategiesApi ("/api/v1",["StrategiesApi"]) function list_strategies(req::HTTP.Request;)::Vector{StrategySummary} + # ... +end +""" +macro registerStrategiesApi(config, func_def) + # Ensure the second argument is a function definition + if !Meta.isexpr(func_def, :function) + error("The second argument must be a function definition") + end + + # Handle the config parameter based on its type + if config isa String || config isa Symbol + # Simple case: only a route prefix + prefix = config + tags = String["StrategiesApi"] + middleware = nothing + interval = nothing + cron = nothing + elseif Meta.isexpr(config, :tuple) + # Tuple form with multiple configuration parameters + prefix = config.args[1] + tags = length(config.args) > 1 ? config.args[2] : String["StrategiesApi"] + middleware = length(config.args) > 2 ? config.args[3] : nothing + interval = length(config.args) > 3 ? config.args[4] : nothing + cron = length(config.args) > 4 ? config.args[5] : nothing + else + error("Invalid configuration parameter format") + end + + func_name, return_type = parse_function_definition(func_def) + + return quote + local result = $(esc(func_def)) + register_api_route( + :StrategiesApi, + result, + $(esc(return_type)); + prefix=$(esc(prefix)), + tags=$(esc(tags)), + middleware=$(middleware), + interval=$(esc(interval)), + cron=$(esc(cron)) + ) + result + end +end + +macro registerStrategiesApi(func_def) + if !Meta.isexpr(func_def, :function) + error("The argument must be a function definition") + end + + func_name, return_type = parse_function_definition(func_def) + + return quote + local result = $(esc(func_def)) + register_api_route( + :StrategiesApi, + result, + $(esc(return_type)); + prefix="/api/v1", + tags=String["StrategiesApi"], + middleware=nothing, + interval=nothing, + cron=nothing + ) + result + end +end + + +export @registerStrategiesApi + diff --git a/backend/src/api/server/src/apis/api_ToolApi.jl b/backend/src/api/server/src/apis/api_ToolApi.jl new file mode 100644 index 00000000..8a8e3fb8 --- /dev/null +++ b/backend/src/api/server/src/apis/api_ToolApi.jl @@ -0,0 +1,119 @@ +# This file was generated by the Julia OpenAPI Code Generator +# Do not modify this file directly. Modify the OpenAPI specification instead. + + +@interface :ToolApi "GET" "/tools" function list_tools(req::HTTP.Request;)::Vector{ToolSummary} + error("Method not implemented") +end + +@generated_handler :ToolApi :list_tools :read function list_tools_read(handler) + function list_tools_read_handler(req::HTTP.Request;)::Vector{ToolSummary} + openapi_params = Dict{String,Any}() + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +@generated_handler :ToolApi :list_tools :validate function list_tools_validate(handler) + function list_tools_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + op = "list_tools" + + return handler(req) + end +end + +@generated_handler :ToolApi :list_tools :invoke function list_tools_invoke(impl; post_invoke=nothing) + function list_tools_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.list_tools(req::HTTP.Request;) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + + +function registerToolApi(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) + HTTP.register!(router, "GET", path_prefix * "/tools", OpenAPI.Servers.middleware(impl, list_tools_read, list_tools_validate, list_tools_invoke; optional_middlewares...)) + return router +end + + +""" +Macro to register a API route. + +Usage examples: +@registerToolApi ("/api/v1",["ToolApi"]) function list_tools(req::HTTP.Request;)::Vector{ToolSummary} + # ... +end +""" +macro registerToolApi(config, func_def) + # Ensure the second argument is a function definition + if !Meta.isexpr(func_def, :function) + error("The second argument must be a function definition") + end + + # Handle the config parameter based on its type + if config isa String || config isa Symbol + # Simple case: only a route prefix + prefix = config + tags = String["ToolApi"] + middleware = nothing + interval = nothing + cron = nothing + elseif Meta.isexpr(config, :tuple) + # Tuple form with multiple configuration parameters + prefix = config.args[1] + tags = length(config.args) > 1 ? config.args[2] : String["ToolApi"] + middleware = length(config.args) > 2 ? config.args[3] : nothing + interval = length(config.args) > 3 ? config.args[4] : nothing + cron = length(config.args) > 4 ? config.args[5] : nothing + else + error("Invalid configuration parameter format") + end + + func_name, return_type = parse_function_definition(func_def) + + return quote + local result = $(esc(func_def)) + register_api_route( + :ToolApi, + result, + $(esc(return_type)); + prefix=$(esc(prefix)), + tags=$(esc(tags)), + middleware=$(middleware), + interval=$(esc(interval)), + cron=$(esc(cron)) + ) + result + end +end + +macro registerToolApi(func_def) + if !Meta.isexpr(func_def, :function) + error("The argument must be a function definition") + end + + func_name, return_type = parse_function_definition(func_def) + + return quote + local result = $(esc(func_def)) + register_api_route( + :ToolApi, + result, + $(esc(return_type)); + prefix="/api/v1", + tags=String["ToolApi"], + middleware=nothing, + interval=nothing, + cron=nothing + ) + result + end +end + + +export @registerToolApi + diff --git a/backend/src/api/server/src/models/model_AgentBlueprint.jl b/backend/src/api/server/src/models/model_AgentBlueprint.jl index 43a4dde4..1b34fee6 100644 --- a/backend/src/api/server/src/models/model_AgentBlueprint.jl +++ b/backend/src/api/server/src/models/model_AgentBlueprint.jl @@ -19,23 +19,37 @@ Base.@kwdef mutable struct AgentBlueprint <: OpenAPI.APIModel strategy = nothing # spec type: Union{ Nothing, StrategyBlueprint } trigger = nothing # spec type: Union{ Nothing, TriggerConfig } - function AgentBlueprint(tools, strategy, trigger, ) - OpenAPI.validate_property(AgentBlueprint, Symbol("tools"), tools) - OpenAPI.validate_property(AgentBlueprint, Symbol("strategy"), strategy) - OpenAPI.validate_property(AgentBlueprint, Symbol("trigger"), trigger) - return new(tools, strategy, trigger, ) + function AgentBlueprint( + tools::Union{Nothing, Vector}, + strategy, + trigger, + ) + o = new(tools, strategy, trigger, ) + OpenAPI.validate_properties(o) + return o end end # type AgentBlueprint +StructTypes.StructType(::AgentBlueprint) = StructTypes.Struct() + const _property_types_AgentBlueprint = Dict{Symbol,String}(Symbol("tools")=>"Vector{ToolBlueprint}", Symbol("strategy")=>"StrategyBlueprint", Symbol("trigger")=>"TriggerConfig", ) OpenAPI.property_type(::Type{ AgentBlueprint }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_AgentBlueprint[name]))} -function check_required(o::AgentBlueprint) +function OpenAPI.check_required(o::AgentBlueprint) o.tools === nothing && (return false) o.strategy === nothing && (return false) o.trigger === nothing && (return false) true end +function OpenAPI.validate_properties(o::AgentBlueprint) + OpenAPI.validate_property(AgentBlueprint, Symbol("tools"), o.tools) + OpenAPI.validate_property(AgentBlueprint, Symbol("strategy"), o.strategy) + OpenAPI.validate_property(AgentBlueprint, Symbol("trigger"), o.trigger) +end + function OpenAPI.validate_property(::Type{ AgentBlueprint }, name::Symbol, val) + + + end diff --git a/backend/src/api/server/src/models/model_AgentSummary.jl b/backend/src/api/server/src/models/model_AgentSummary.jl index b481a8f1..36cdaf86 100644 --- a/backend/src/api/server/src/models/model_AgentSummary.jl +++ b/backend/src/api/server/src/models/model_AgentSummary.jl @@ -16,24 +16,37 @@ Base.@kwdef mutable struct AgentSummary <: OpenAPI.APIModel id::Union{Nothing, String} = nothing state::Union{Nothing, String} = nothing - function AgentSummary(id, state, ) - OpenAPI.validate_property(AgentSummary, Symbol("id"), id) - OpenAPI.validate_property(AgentSummary, Symbol("state"), state) - return new(id, state, ) + function AgentSummary( + id::Union{Nothing, String}, + state::Union{Nothing, String}, + ) + o = new(id, state, ) + OpenAPI.validate_properties(o) + return o end end # type AgentSummary +StructTypes.StructType(::AgentSummary) = StructTypes.Struct() + const _property_types_AgentSummary = Dict{Symbol,String}(Symbol("id")=>"String", Symbol("state")=>"String", ) OpenAPI.property_type(::Type{ AgentSummary }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_AgentSummary[name]))} -function check_required(o::AgentSummary) +function OpenAPI.check_required(o::AgentSummary) o.id === nothing && (return false) o.state === nothing && (return false) true end +function OpenAPI.validate_properties(o::AgentSummary) + OpenAPI.validate_property(AgentSummary, Symbol("id"), o.id) + OpenAPI.validate_property(AgentSummary, Symbol("state"), o.state) +end + function OpenAPI.validate_property(::Type{ AgentSummary }, name::Symbol, val) + + if name === Symbol("state") OpenAPI.validate_param(name, "AgentSummary", :enum, val, ["CREATED", "RUNNING", "PAUSED", "STOPPED"]) end + end diff --git a/backend/src/api/server/src/models/model_AgentUpdate.jl b/backend/src/api/server/src/models/model_AgentUpdate.jl index faba9700..91b904ee 100644 --- a/backend/src/api/server/src/models/model_AgentUpdate.jl +++ b/backend/src/api/server/src/models/model_AgentUpdate.jl @@ -13,22 +13,33 @@ Base.@kwdef mutable struct AgentUpdate <: OpenAPI.APIModel state::Union{Nothing, String} = nothing - function AgentUpdate(state, ) - OpenAPI.validate_property(AgentUpdate, Symbol("state"), state) - return new(state, ) + function AgentUpdate( + state::Union{Nothing, String}, + ) + o = new(state, ) + OpenAPI.validate_properties(o) + return o end end # type AgentUpdate +StructTypes.StructType(::AgentUpdate) = StructTypes.Struct() + const _property_types_AgentUpdate = Dict{Symbol,String}(Symbol("state")=>"String", ) OpenAPI.property_type(::Type{ AgentUpdate }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_AgentUpdate[name]))} -function check_required(o::AgentUpdate) +function OpenAPI.check_required(o::AgentUpdate) o.state === nothing && (return false) true end +function OpenAPI.validate_properties(o::AgentUpdate) + OpenAPI.validate_property(AgentUpdate, Symbol("state"), o.state) +end + function OpenAPI.validate_property(::Type{ AgentUpdate }, name::Symbol, val) + if name === Symbol("state") OpenAPI.validate_param(name, "AgentUpdate", :enum, val, ["RUNNING", "PAUSED", "STOPPED"]) end + end diff --git a/backend/src/api/server/src/models/model_CreateAgentRequest.jl b/backend/src/api/server/src/models/model_CreateAgentRequest.jl index aea7ccf7..78924de0 100644 --- a/backend/src/api/server/src/models/model_CreateAgentRequest.jl +++ b/backend/src/api/server/src/models/model_CreateAgentRequest.jl @@ -16,21 +16,33 @@ Base.@kwdef mutable struct CreateAgentRequest <: OpenAPI.APIModel id::Union{Nothing, String} = nothing blueprint = nothing # spec type: Union{ Nothing, AgentBlueprint } - function CreateAgentRequest(id, blueprint, ) - OpenAPI.validate_property(CreateAgentRequest, Symbol("id"), id) - OpenAPI.validate_property(CreateAgentRequest, Symbol("blueprint"), blueprint) - return new(id, blueprint, ) + function CreateAgentRequest( + id::Union{Nothing, String}, + blueprint, + ) + o = new(id, blueprint, ) + OpenAPI.validate_properties(o) + return o end end # type CreateAgentRequest +StructTypes.StructType(::CreateAgentRequest) = StructTypes.Struct() + const _property_types_CreateAgentRequest = Dict{Symbol,String}(Symbol("id")=>"String", Symbol("blueprint")=>"AgentBlueprint", ) OpenAPI.property_type(::Type{ CreateAgentRequest }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_CreateAgentRequest[name]))} -function check_required(o::CreateAgentRequest) +function OpenAPI.check_required(o::CreateAgentRequest) o.id === nothing && (return false) o.blueprint === nothing && (return false) true end +function OpenAPI.validate_properties(o::CreateAgentRequest) + OpenAPI.validate_property(CreateAgentRequest, Symbol("id"), o.id) + OpenAPI.validate_property(CreateAgentRequest, Symbol("blueprint"), o.blueprint) +end + function OpenAPI.validate_property(::Type{ CreateAgentRequest }, name::Symbol, val) + + end diff --git a/backend/src/api/server/src/models/model_StrategyBlueprint.jl b/backend/src/api/server/src/models/model_StrategyBlueprint.jl index f8120095..c0cd0bf0 100644 --- a/backend/src/api/server/src/models/model_StrategyBlueprint.jl +++ b/backend/src/api/server/src/models/model_StrategyBlueprint.jl @@ -16,21 +16,33 @@ Base.@kwdef mutable struct StrategyBlueprint <: OpenAPI.APIModel name::Union{Nothing, String} = nothing config::Union{Nothing, Dict{String, Any}} = nothing - function StrategyBlueprint(name, config, ) - OpenAPI.validate_property(StrategyBlueprint, Symbol("name"), name) - OpenAPI.validate_property(StrategyBlueprint, Symbol("config"), config) - return new(name, config, ) + function StrategyBlueprint( + name::Union{Nothing, String}, + config::Union{Nothing, Dict{String, Any}}, + ) + o = new(name, config, ) + OpenAPI.validate_properties(o) + return o end end # type StrategyBlueprint +StructTypes.StructType(::StrategyBlueprint) = StructTypes.Struct() + const _property_types_StrategyBlueprint = Dict{Symbol,String}(Symbol("name")=>"String", Symbol("config")=>"Dict{String, Any}", ) OpenAPI.property_type(::Type{ StrategyBlueprint }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_StrategyBlueprint[name]))} -function check_required(o::StrategyBlueprint) +function OpenAPI.check_required(o::StrategyBlueprint) o.name === nothing && (return false) o.config === nothing && (return false) true end +function OpenAPI.validate_properties(o::StrategyBlueprint) + OpenAPI.validate_property(StrategyBlueprint, Symbol("name"), o.name) + OpenAPI.validate_property(StrategyBlueprint, Symbol("config"), o.config) +end + function OpenAPI.validate_property(::Type{ StrategyBlueprint }, name::Symbol, val) + + end diff --git a/backend/src/api/server/src/models/model_StrategySummary.jl b/backend/src/api/server/src/models/model_StrategySummary.jl index 5f8b4ab4..4d4c0d5e 100644 --- a/backend/src/api/server/src/models/model_StrategySummary.jl +++ b/backend/src/api/server/src/models/model_StrategySummary.jl @@ -13,19 +13,29 @@ Base.@kwdef mutable struct StrategySummary <: OpenAPI.APIModel name::Union{Nothing, String} = nothing - function StrategySummary(name, ) - OpenAPI.validate_property(StrategySummary, Symbol("name"), name) - return new(name, ) + function StrategySummary( + name::Union{Nothing, String}, + ) + o = new(name, ) + OpenAPI.validate_properties(o) + return o end end # type StrategySummary +StructTypes.StructType(::StrategySummary) = StructTypes.Struct() + const _property_types_StrategySummary = Dict{Symbol,String}(Symbol("name")=>"String", ) OpenAPI.property_type(::Type{ StrategySummary }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_StrategySummary[name]))} -function check_required(o::StrategySummary) +function OpenAPI.check_required(o::StrategySummary) o.name === nothing && (return false) true end +function OpenAPI.validate_properties(o::StrategySummary) + OpenAPI.validate_property(StrategySummary, Symbol("name"), o.name) +end + function OpenAPI.validate_property(::Type{ StrategySummary }, name::Symbol, val) + end diff --git a/backend/src/api/server/src/models/model_ToolBlueprint.jl b/backend/src/api/server/src/models/model_ToolBlueprint.jl index ff588a82..cea693b5 100644 --- a/backend/src/api/server/src/models/model_ToolBlueprint.jl +++ b/backend/src/api/server/src/models/model_ToolBlueprint.jl @@ -16,21 +16,33 @@ Base.@kwdef mutable struct ToolBlueprint <: OpenAPI.APIModel name::Union{Nothing, String} = nothing config::Union{Nothing, Dict{String, Any}} = nothing - function ToolBlueprint(name, config, ) - OpenAPI.validate_property(ToolBlueprint, Symbol("name"), name) - OpenAPI.validate_property(ToolBlueprint, Symbol("config"), config) - return new(name, config, ) + function ToolBlueprint( + name::Union{Nothing, String}, + config::Union{Nothing, Dict{String, Any}}, + ) + o = new(name, config, ) + OpenAPI.validate_properties(o) + return o end end # type ToolBlueprint +StructTypes.StructType(::ToolBlueprint) = StructTypes.Struct() + const _property_types_ToolBlueprint = Dict{Symbol,String}(Symbol("name")=>"String", Symbol("config")=>"Dict{String, Any}", ) OpenAPI.property_type(::Type{ ToolBlueprint }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_ToolBlueprint[name]))} -function check_required(o::ToolBlueprint) +function OpenAPI.check_required(o::ToolBlueprint) o.name === nothing && (return false) o.config === nothing && (return false) true end +function OpenAPI.validate_properties(o::ToolBlueprint) + OpenAPI.validate_property(ToolBlueprint, Symbol("name"), o.name) + OpenAPI.validate_property(ToolBlueprint, Symbol("config"), o.config) +end + function OpenAPI.validate_property(::Type{ ToolBlueprint }, name::Symbol, val) + + end diff --git a/backend/src/api/server/src/models/model_ToolSummary.jl b/backend/src/api/server/src/models/model_ToolSummary.jl index cec18519..3ba8d460 100644 --- a/backend/src/api/server/src/models/model_ToolSummary.jl +++ b/backend/src/api/server/src/models/model_ToolSummary.jl @@ -16,20 +16,32 @@ Base.@kwdef mutable struct ToolSummary <: OpenAPI.APIModel name::Union{Nothing, String} = nothing metadata = nothing # spec type: Union{ Nothing, ToolSummaryMetadata } - function ToolSummary(name, metadata, ) - OpenAPI.validate_property(ToolSummary, Symbol("name"), name) - OpenAPI.validate_property(ToolSummary, Symbol("metadata"), metadata) - return new(name, metadata, ) + function ToolSummary( + name::Union{Nothing, String}, + metadata, + ) + o = new(name, metadata, ) + OpenAPI.validate_properties(o) + return o end end # type ToolSummary +StructTypes.StructType(::ToolSummary) = StructTypes.Struct() + const _property_types_ToolSummary = Dict{Symbol,String}(Symbol("name")=>"String", Symbol("metadata")=>"ToolSummaryMetadata", ) OpenAPI.property_type(::Type{ ToolSummary }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_ToolSummary[name]))} -function check_required(o::ToolSummary) +function OpenAPI.check_required(o::ToolSummary) o.name === nothing && (return false) true end +function OpenAPI.validate_properties(o::ToolSummary) + OpenAPI.validate_property(ToolSummary, Symbol("name"), o.name) + OpenAPI.validate_property(ToolSummary, Symbol("metadata"), o.metadata) +end + function OpenAPI.validate_property(::Type{ ToolSummary }, name::Symbol, val) + + end diff --git a/backend/src/api/server/src/models/model_ToolSummaryMetadata.jl b/backend/src/api/server/src/models/model_ToolSummaryMetadata.jl index 927d5e5c..bccc2efc 100644 --- a/backend/src/api/server/src/models/model_ToolSummaryMetadata.jl +++ b/backend/src/api/server/src/models/model_ToolSummaryMetadata.jl @@ -13,18 +13,28 @@ Base.@kwdef mutable struct ToolSummaryMetadata <: OpenAPI.APIModel description::Union{Nothing, String} = nothing - function ToolSummaryMetadata(description, ) - OpenAPI.validate_property(ToolSummaryMetadata, Symbol("description"), description) - return new(description, ) + function ToolSummaryMetadata( + description::Union{Nothing, String}, + ) + o = new(description, ) + OpenAPI.validate_properties(o) + return o end end # type ToolSummaryMetadata +StructTypes.StructType(::ToolSummaryMetadata) = StructTypes.Struct() + const _property_types_ToolSummaryMetadata = Dict{Symbol,String}(Symbol("description")=>"String", ) OpenAPI.property_type(::Type{ ToolSummaryMetadata }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_ToolSummaryMetadata[name]))} -function check_required(o::ToolSummaryMetadata) +function OpenAPI.check_required(o::ToolSummaryMetadata) true end +function OpenAPI.validate_properties(o::ToolSummaryMetadata) + OpenAPI.validate_property(ToolSummaryMetadata, Symbol("description"), o.description) +end + function OpenAPI.validate_property(::Type{ ToolSummaryMetadata }, name::Symbol, val) + end diff --git a/backend/src/api/server/src/models/model_TriggerConfig.jl b/backend/src/api/server/src/models/model_TriggerConfig.jl index fd9c3007..106757fc 100644 --- a/backend/src/api/server/src/models/model_TriggerConfig.jl +++ b/backend/src/api/server/src/models/model_TriggerConfig.jl @@ -16,24 +16,37 @@ Base.@kwdef mutable struct TriggerConfig <: OpenAPI.APIModel type::Union{Nothing, String} = nothing params::Union{Nothing, Dict{String, Any}} = nothing - function TriggerConfig(type, params, ) - OpenAPI.validate_property(TriggerConfig, Symbol("type"), type) - OpenAPI.validate_property(TriggerConfig, Symbol("params"), params) - return new(type, params, ) + function TriggerConfig( + type::Union{Nothing, String}, + params::Union{Nothing, Dict{String, Any}}, + ) + o = new(type, params, ) + OpenAPI.validate_properties(o) + return o end end # type TriggerConfig +StructTypes.StructType(::TriggerConfig) = StructTypes.Struct() + const _property_types_TriggerConfig = Dict{Symbol,String}(Symbol("type")=>"String", Symbol("params")=>"Dict{String, Any}", ) OpenAPI.property_type(::Type{ TriggerConfig }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_TriggerConfig[name]))} -function check_required(o::TriggerConfig) +function OpenAPI.check_required(o::TriggerConfig) o.type === nothing && (return false) o.params === nothing && (return false) true end +function OpenAPI.validate_properties(o::TriggerConfig) + OpenAPI.validate_property(TriggerConfig, Symbol("type"), o.type) + OpenAPI.validate_property(TriggerConfig, Symbol("params"), o.params) +end + function OpenAPI.validate_property(::Type{ TriggerConfig }, name::Symbol, val) + if name === Symbol("type") OpenAPI.validate_param(name, "TriggerConfig", :enum, val, ["webhook"]) end + + end diff --git a/backend/src/api/spec/api-spec.yaml b/backend/src/api/spec/api-spec.yaml index c6ad9339..c1bb2717 100644 --- a/backend/src/api/spec/api-spec.yaml +++ b/backend/src/api/spec/api-spec.yaml @@ -3,15 +3,24 @@ info: title: JuliaOS API description: API for JuliaOS backend server version: 0.1.0 + license: + name: MIT + url: https://opensource.org/licenses/MIT servers: - url: /api/v1 - + +security: + - ApiKeyAuth: [] + - BearerAuth: [] + paths: /agents: get: operationId: listAgents summary: Get all agents + tags: + - Agent description: Retrieve a list of all agents responses: '200': @@ -22,9 +31,13 @@ paths: type: array items: $ref: '#/components/schemas/AgentSummary' + '400': + description: Bad request post: operationId: createAgent summary: Create a new agent + tags: + - Agent description: Create a new agent with the provided details requestBody: required: true @@ -48,11 +61,15 @@ paths: application/json: schema: $ref: '#/components/schemas/AgentSummary' + '400': + description: Bad request /agents/{agent_id}: get: operationId: getAgent summary: Get a specific agent + tags: + - Agent description: Retrieve the details of a specific agent by ID parameters: - name: agent_id @@ -73,6 +90,8 @@ paths: put: operationId: updateAgent summary: Update a specific agent + tags: + - Agent description: Update the details of a specific agent by ID parameters: - name: agent_id @@ -99,6 +118,8 @@ paths: delete: operationId: deleteAgent summary: Delete a specific agent + tags: + - Agent description: Delete a specific agent by ID parameters: - name: agent_id @@ -117,6 +138,8 @@ paths: post: operationId: processAgentWebhook summary: Trigger event-based agents and provide data to them + tags: + - Agent description: Process a webhook event for a specific agent by ID parameters: - name: agent_id @@ -145,6 +168,8 @@ paths: get: operationId: getAgentLogs summary: Get the logs of a specific agent + tags: + - Agent description: Retrieve the logs of a specific agent by ID parameters: - name: agent_id @@ -168,6 +193,8 @@ paths: get: operationId: getAgentOutput summary: Get the output of a specific agent + tags: + - Agent description: Retrieve the output of a specific agent by ID parameters: - name: agent_id @@ -191,6 +218,8 @@ paths: get: operationId: listTools summary: Get a list of all tools available for use by agents in the system + tags: + - Tool responses: '200': description: A list of tools @@ -200,11 +229,15 @@ paths: type: array items: $ref: '#/components/schemas/ToolSummary' + '400': + description: Bad request /strategies: get: operationId: listStrategies summary: Get a list of all strategies available for use by agents in the system + tags: + - Strategies responses: '200': description: A list of strategies @@ -214,8 +247,20 @@ paths: type: array items: $ref: '#/components/schemas/StrategySummary' + '400': + description: Bad request components: + securitySchemes: + ApiKeyAuth: + type: apiKey + in: header + name: X-API-Key + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + schemas: AgentSummary: type: object diff --git a/backend/src/api/spec/templates/README.mustache b/backend/src/api/spec/templates/README.mustache new file mode 100644 index 00000000..cf657260 --- /dev/null +++ b/backend/src/api/spec/templates/README.mustache @@ -0,0 +1,66 @@ +# Julia API server for {{packageName}} + +{{#appDescriptionWithNewLines}} +{{{.}}} +{{/appDescriptionWithNewLines}} + +## Overview +This API server was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API client. + +- API version: {{appVersion}} +{{^hideGenerationTimestamp}} +- Build date: {{generatedDate}} +{{/hideGenerationTimestamp}} +- Generator version: {{generatorVersion}} +- Build package: {{generatorClass}} +{{#infoUrl}} +For more information, please visit [{{{infoUrl}}}]({{{infoUrl}}}) +{{/infoUrl}} + + +## Installation +Place the Julia files generated under the `src` folder in your Julia project. Include {{packageName}}.jl in the project code. +It would include the module named {{packageName}}. + +Implement the server methods as listed below. They are also documented with the {{packageName}} module. +Launch a HTTP server with a router that has all handlers registered. A `register` method is provided in {{packageName}} module for convenience. + +```julia +register( + router::HTTP.Router, # Router to register handlers in + impl; # Module that implements the server methods + path_prefix::String="", # Prefix to be applied to all paths + optional_middlewares... # Optional middlewares to be applied to all handlers +) +``` + +Optional middlewares can be one or more of: +- `init`: called before the request is processed +- `pre_validation`: called after the request is parsed but before validation +- `pre_invoke`: called after validation but before the handler is invoked +- `post_invoke`: called after the handler is invoked but before the response is sent + +The order in which middlewares are invoked are: +`init |> read |> pre_validation |> validate |> pre_invoke |> invoke |> post_invoke` + + +## API Endpoints + +The following server methods must be implemented: + +Class | Method | HTTP request | Description +------------ | ------------- | ------------- | ------------- +{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}*{{classname}}* | [**{{operationId}}**]({{apiDocPath}}{{classname}}.md#{{operationIdLowerCase}}) | **{{httpMethod}}** {{path}} | {{summary}} +{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} + + +## Models + +{{#models}}{{#model}} - [{{{classname}}}]({{modelDocPath}}{{{classname}}}.md) +{{/model}}{{/models}} + + +## Author + +{{#apiInfo}}{{#apis}}{{#-last}}{{infoEmail}} +{{/-last}}{{/apis}}{{/apiInfo}} diff --git a/backend/src/api/spec/templates/api.mustache b/backend/src/api/spec/templates/api.mustache new file mode 100644 index 00000000..6bb715d9 --- /dev/null +++ b/backend/src/api/spec/templates/api.mustache @@ -0,0 +1,160 @@ +{{>partial_header}} +{{#operations}} + +{{#operation}} +@interface :{{classname}} "{{httpMethod}}" "{{{path}}}" function {{operationId}}(req::HTTP.Request{{#allParams}}{{#required}}, {{paramName}}::{{#isBinary}}Vector{UInt8}{{/isBinary}}{{^isBinary}}{{dataType}}{{/isBinary}}{{/required}}{{/allParams}};{{#allParams}}{{^required}} {{paramName}}=nothing,{{/required}}{{/allParams}})::{{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}Any{{/returnType}} + error("Method not implemented") +end +{{/operation}} + +{{#operation}} +@generated_handler :{{classname}} :{{operationId}} :read function {{operationId}}_read(handler) + function {{operationId}}_read_handler(req::HTTP.Request{{#allParams}}{{#required}}, {{paramName}}::{{#isBinary}}Vector{UInt8}{{/isBinary}}{{^isBinary}}{{dataType}}{{/isBinary}}{{/required}}{{/allParams}};{{#allParams}}{{^required}} {{paramName}}=nothing,{{/required}}{{/allParams}})::{{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}Any{{/returnType}} + openapi_params = Dict{String,Any}(){{#hasPathParams}} + path_params = HTTP.getparams(req){{#pathParams}} + openapi_params["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"] = OpenAPI.Servers.to_param({{dataType}}, path_params, "{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}", {{#required}}required=true, {{/required}}{{#isListContainer}}collection_format="{{collectionFormat}}", {{/isListContainer}}){{/pathParams}}{{/hasPathParams}}{{#hasQueryParams}} + query_params = HTTP.queryparams(URIs.URI(req.target)){{#queryParams}} + openapi_params["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"] = OpenAPI.Servers.to_param({{dataType}}, query_params, "{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}", {{#required}}required=true, {{/required}}style="{{style}}", is_explode={{isExplode}}{{#isListContainer}},collection_format="{{collectionFormat}}"{{/isListContainer}}){{/queryParams}}{{/hasQueryParams}}{{#hasHeaderParams}} + headers = Dict{String,String}(HTTP.headers(req)){{#headerParams}} + openapi_params["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"] = OpenAPI.Servers.to_param({{dataType}}, headers, "{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}", {{#required}}required=true, {{/required}}{{#isListContainer}}collection_format="{{collectionFormat}}", {{/isListContainer}}){{/headerParams}}{{/hasHeaderParams}}{{#hasBodyParam}}{{#bodyParams}} + openapi_params["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"] = OpenAPI.Servers.to_param_type({{dataType}}, String(req.body)){{/bodyParams}}{{/hasBodyParam}}{{#hasFormParams}} + ismultipart = {{#isMultipart}}true{{/isMultipart}}{{^isMultipart}}false{{/isMultipart}} + form_data = ismultipart ? HTTP.parse_multipart_form(req) : HTTP.queryparams(String(copy(req.body))){{#formParams}} + openapi_params["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"] = OpenAPI.Servers.to_param({{#dataType}}{{#isBinary}}Vector{UInt8}{{/isBinary}}{{^isBinary}}{{dataType}}{{/isBinary}}{{/dataType}}, form_data, "{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"; multipart=ismultipart, isfile={{#isFile}}true{{/isFile}}{{^isFile}}false{{/isFile}}, {{#required}}required=true, {{/required}}{{#isListContainer}}collection_format="{{collectionFormat}}", {{/isListContainer}}){{/formParams}}{{/hasFormParams}} + req.context[:openapi_params] = openapi_params + + return handler(req) + end +end + +@generated_handler :{{classname}} :{{operationId}} :validate function {{operationId}}_validate(handler) + function {{operationId}}_validate_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + op = "{{operationId}}"{{#allParams}} + + n = "{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}" + v = get(openapi_params, n, nothing){{#required}} + isnothing(v) && throw(OpenAPI.ValidationException(;reason="missing parameter $n", operation_or_model=op)){{/required}} + if !isnothing(v){{#hasValidation}}{{#maxLength}} + OpenAPI.validate_param(n, op, :maxLength, v, {{maxLength}}){{/maxLength}}{{#minLength}} + OpenAPI.validate_param(n, op, :minLength, v, {{minLength}}){{/minLength}}{{#maximum}} + OpenAPI.validate_param(n, op, :maximum, v, {{maximum}}, {{#exclusiveMaximum}}true{{/exclusiveMaximum}}{{^exclusiveMaximum}}false{{/exclusiveMaximum}}){{/maximum}}{{#minimum}} + OpenAPI.validate_param(n, op, :minimum, v, {{minimum}}, {{#exclusiveMinimum}}true{{/exclusiveMinimum}}{{^exclusiveMinimum}}false{{/exclusiveMinimum}}){{/minimum}}{{#maxItems}} + OpenAPI.validate_param(n, op, :maxItems, v, {{maxItems}}){{/maxItems}}{{#minItems}} + OpenAPI.validate_param(n, op, :minItems, v, {{minItems}}){{/minItems}}{{#uniqueItems}} + OpenAPI.validate_param(n, op, :uniqueItems, v, {{uniqueItems}}){{/uniqueItems}}{{#maxProperties}} + OpenAPI.validate_param(n, op, :maxProperties, v, {{maxProperties}}){{/maxProperties}}{{#minProperties}} + OpenAPI.validate_param(n, op, :minProperties, v, {{minProperties}}){{/minProperties}}{{#pattern}} + OpenAPI.validate_param(n, op, :pattern, v, r"{{{pattern}}}"){{/pattern}}{{#multipleOf}} + OpenAPI.validate_param(n, op, :multipleOf, v, {{multipleOf}}){{/multipleOf}}{{/hasValidation}}{{^hasValidation}} + if isa(v, OpenAPI.APIModel) + OpenAPI.validate_properties(v) + if !OpenAPI.check_required(v) + throw(OpenAPI.ValidationException(;reason="$n is missing required properties", operation_or_model=op)) + end + end{{/hasValidation}} + end{{/allParams}} + + return handler(req) + end +end + +@generated_handler :{{classname}} :{{operationId}} :invoke function {{operationId}}_invoke(impl; post_invoke=nothing) + function {{operationId}}_invoke_handler(req::HTTP.Request) + openapi_params = req.context[:openapi_params] + ret = impl.{{operationId}}(req::HTTP.Request{{#allParams}}{{#required}}, openapi_params["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"]{{/required}}{{/allParams}};{{#allParams}}{{^required}} {{paramName}}=get(openapi_params, "{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}", nothing),{{/required}}{{/allParams}}) + resp = OpenAPI.Servers.server_response(ret) + return (post_invoke === nothing) ? resp : post_invoke(req, resp) + end +end + +{{/operation}} + +function register{{classname}}(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) + {{#operation}} + HTTP.register!(router, "{{httpMethod}}", path_prefix * "{{{path}}}", OpenAPI.Servers.middleware(impl, {{operationId}}_read, {{operationId}}_validate, {{operationId}}_invoke; optional_middlewares...)) + {{/operation}} + return router +end + + +""" +Macro to register a API route. + +Usage examples: +{{#operation}} +@register{{classname}} ("{{basePath}}",["{{classname}}"]) function {{operationId}}(req::HTTP.Request{{#allParams}}{{#required}}, {{paramName}}::{{#isBinary}}Vector{UInt8}{{/isBinary}}{{^isBinary}}{{dataType}}{{/isBinary}}{{/required}}{{/allParams}};{{#allParams}}{{^required}} {{paramName}}=nothing,{{/required}}{{/allParams}})::{{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}Any{{/returnType}} + # ... +end +{{/operation}} +""" +macro register{{classname}}(config, func_def) + # Ensure the second argument is a function definition + if !Meta.isexpr(func_def, :function) + error("The second argument must be a function definition") + end + + # Handle the config parameter based on its type + if config isa String || config isa Symbol + # Simple case: only a route prefix + prefix = config + tags = String["{{classname}}"] + middleware = nothing + interval = nothing + cron = nothing + elseif Meta.isexpr(config, :tuple) + # Tuple form with multiple configuration parameters + prefix = config.args[1] + tags = length(config.args) > 1 ? config.args[2] : String["{{classname}}"] + middleware = length(config.args) > 2 ? config.args[3] : nothing + interval = length(config.args) > 3 ? config.args[4] : nothing + cron = length(config.args) > 4 ? config.args[5] : nothing + else + error("Invalid configuration parameter format") + end + + func_name, return_type = parse_function_definition(func_def) + + return quote + local result = $(esc(func_def)) + register_api_route( + :{{classname}}, + result, + $(esc(return_type)); + prefix=$(esc(prefix)), + tags=$(esc(tags)), + middleware=$(middleware), + interval=$(esc(interval)), + cron=$(esc(cron)) + ) + result + end +end + +macro register{{classname}}(func_def) + if !Meta.isexpr(func_def, :function) + error("The argument must be a function definition") + end + + func_name, return_type = parse_function_definition(func_def) + + return quote + local result = $(esc(func_def)) + register_api_route( + :{{classname}}, + result, + $(esc(return_type)); + prefix="{{basePath}}", + tags=String["{{classname}}"], + middleware=nothing, + interval=nothing, + cron=nothing + ) + result + end +end + + +export @register{{classname}} + +{{/operations}} diff --git a/backend/src/api/spec/templates/api_doc.mustache b/backend/src/api/spec/templates/api_doc.mustache new file mode 100644 index 00000000..19562f37 --- /dev/null +++ b/backend/src/api/spec/templates/api_doc.mustache @@ -0,0 +1,49 @@ +# {{classname}}{{#description}} +{{.}}{{/description}} + +All URIs are relative to *{{basePath}}* + +Method | HTTP request | Description +------------- | ------------- | ------------- +{{#operations}}{{#operation}}[**{{operationId}}**]({{classname}}.md#{{operationId}}) | **{{httpMethod}}** {{path}} | {{summary}} +{{/operation}}{{/operations}} + +{{#operations}} +{{#operation}} +# **{{{operationId}}}** +> {{operationId}}(req::HTTP.Request{{#allParams}}{{#required}}, {{paramName}}::{{#dataType}}{{#isBinary}}Vector{UInt8}{{/isBinary}}{{^isBinary}}{{dataType}}{{/isBinary}}{{/dataType}}{{/required}}{{/allParams}};{{#allParams}}{{^required}} {{paramName}}=nothing,{{/required}}{{/allParams}}) -> {{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}Nothing{{/returnType}} + +{{{summary}}}{{#notes}} + +{{{.}}}{{/notes}} + +### Required Parameters +{{^allParams}}This endpoint does not need any parameter.{{/allParams}}{{#allParams}}{{#-last}} +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **req** | **HTTP.Request** | The HTTP Request object | {{/-last}}{{/allParams}}{{#allParams}}{{#required}} +**{{paramName}}** | {{#isPrimitiveType}}**{{#isBinary}}Vector{UInt8}{{/isBinary}}{{^isBinary}}{{dataType}}{{/isBinary}}**{{/isPrimitiveType}}{{^isPrimitiveType}}{{^isFile}}[**{{dataType}}**]({{baseType}}.md){{/isFile}}{{#isFile}}**{{#isBinary}}Vector{UInt8}{{/isBinary}}{{^isBinary}}{{dataType}}{{/isBinary}}**{{/isFile}}{{/isPrimitiveType}}| {{description}} |{{/required}}{{/allParams}}{{#hasOptionalParams}} + +### Optional Parameters +{{#allParams}}{{#-last}} +Name | Type | Description | Notes +------------- | ------------- | ------------- | -------------{{/-last}}{{/allParams}}{{#allParams}}{{^required}} + **{{paramName}}** | {{#isPrimitiveType}}**{{#isBinary}}Vector{UInt8}{{/isBinary}}{{^isBinary}}{{dataType}}{{/isBinary}}**{{/isPrimitiveType}}{{^isPrimitiveType}}{{^isFile}}[**{{dataType}}**]({{baseType}}.md){{/isFile}}{{#isFile}}**{{#isBinary}}Vector{UInt8}{{/isBinary}}{{^isBinary}}{{dataType}}{{/isBinary}}**{{/isFile}}{{/isPrimitiveType}}| {{description}} | {{^isBinary}}{{#defaultValue}}[default to {{.}}]{{/defaultValue}}{{/isBinary}}{{/required}}{{/allParams}}{{/hasOptionalParams}} + +### Return type + +{{#returnType}}{{#returnTypeIsPrimitive}}**{{{returnType}}}**{{/returnTypeIsPrimitive}}{{^returnTypeIsPrimitive}}[**{{{returnType}}}**]({{returnBaseType}}.md){{/returnTypeIsPrimitive}}{{/returnType}}{{^returnType}}Nothing{{/returnType}} + +### Authorization + +{{^authMethods}}No authorization required{{/authMethods}}{{#authMethods}}[{{{name}}}](../README.md#{{{name}}}){{^-last}}, {{/-last}}{{/authMethods}} + +### HTTP request headers + + - **Content-Type**: {{#consumes}}{{{mediaType}}}{{^-last}}, {{/-last}}{{/consumes}}{{^consumes}}Not defined{{/consumes}} + - **Accept**: {{#produces}}{{{mediaType}}}{{^-last}}, {{/-last}}{{/produces}}{{^produces}}Not defined{{/produces}} + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +{{/operation}} +{{/operations}} diff --git a/backend/src/api/spec/templates/model.mustache b/backend/src/api/spec/templates/model.mustache new file mode 100644 index 00000000..a6165354 --- /dev/null +++ b/backend/src/api/spec/templates/model.mustache @@ -0,0 +1,15 @@ +{{>partial_header}} +{{#models}} +{{#model}} +{{#isAlias}} +{{>partial_model_alias}} +{{/isAlias}}{{^isAlias}}{{#oneOf}}{{#-first}} +{{>partial_model_oneof}} +{{/-first}}{{/oneOf}}{{^oneOf}}{{#anyOf}}{{#-first}} +{{>partial_model_anyof}} +{{/-first}}{{/anyOf}}{{^anyOf}}{{#hasVars}} +{{>partial_model_single}} +{{/hasVars}}{{^hasVars}} +{{>partial_model_alias}} +{{/hasVars}} +{{/anyOf}}{{/oneOf}}{{/isAlias}}{{/model}}{{/models}} \ No newline at end of file diff --git a/backend/src/api/spec/templates/model_doc.mustache b/backend/src/api/spec/templates/model_doc.mustache new file mode 100644 index 00000000..c5e99b3c --- /dev/null +++ b/backend/src/api/spec/templates/model_doc.mustache @@ -0,0 +1,17 @@ +{{#models}}{{#model}}# {{classname}} + +{{#oneOf}}{{#-first}} +{{>partial_model_doc_oneof}} +{{/-first}}{{/oneOf}}{{^oneOf}}{{#anyOf}}{{#-first}} +{{>partial_model_doc_anyof}} +{{/-first}}{{/anyOf}}{{^anyOf}} +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +{{#vars}}**{{name}}** | {{#isPrimitiveType}}**{{{dataType}}}**{{/isPrimitiveType}}{{^isPrimitiveType}}{{#isDateTime}}**{{{dataType}}}**{{/isDateTime}}{{^isDateTime}}[**{{^isContainer}}*{{/isContainer}}{{{dataType}}}**]({{complexType}}.md){{/isDateTime}}{{/isPrimitiveType}} | {{description}} | {{^required}}[optional] {{/required}}{{#isReadOnly}}[readonly] {{/isReadOnly}}{{#defaultValue}}[default to {{{.}}}]{{/defaultValue}} +{{/vars}} +{{/anyOf}}{{/oneOf}} + +[[Back to Model list]](../README.md#models) [[Back to API list]](../README.md#api-endpoints) [[Back to README]](../README.md) + +{{/model}}{{/models}} diff --git a/backend/src/api/spec/templates/modelincludes.mustache b/backend/src/api/spec/templates/modelincludes.mustache new file mode 100644 index 00000000..5e4d8e05 --- /dev/null +++ b/backend/src/api/spec/templates/modelincludes.mustache @@ -0,0 +1,4 @@ +{{>partial_header}} +{{#models}} +{{#model}}include("models/model_{{classname}}.jl"){{/model}} +{{/models}} diff --git a/backend/src/api/spec/templates/partial_header.mustache b/backend/src/api/spec/templates/partial_header.mustache new file mode 100644 index 00000000..e94d7be6 --- /dev/null +++ b/backend/src/api/spec/templates/partial_header.mustache @@ -0,0 +1,2 @@ +# This file was generated by the Julia OpenAPI Code Generator +# Do not modify this file directly. Modify the OpenAPI specification instead. diff --git a/backend/src/api/spec/templates/partial_model_alias.mustache b/backend/src/api/spec/templates/partial_model_alias.mustache new file mode 100644 index 00000000..88601ce0 --- /dev/null +++ b/backend/src/api/spec/templates/partial_model_alias.mustache @@ -0,0 +1,5 @@ +if !isdefined(@__MODULE__, :{{classname}}) + const {{classname}} = {{dataType}} +else + @warn("Skipping redefinition of {{classname}} to {{dataType}}") +end \ No newline at end of file diff --git a/backend/src/api/spec/templates/partial_model_anyof.mustache b/backend/src/api/spec/templates/partial_model_anyof.mustache new file mode 100644 index 00000000..dc7846c4 --- /dev/null +++ b/backend/src/api/spec/templates/partial_model_anyof.mustache @@ -0,0 +1,23 @@ +{{#anyOf}}{{#-first}} +@doc raw"""{{name}}{{#description}} +{{description}}{{/description}} + + {{classname}}(; value=nothing) +""" +mutable struct {{classname}} <: OpenAPI.AnyOfAPIModel + value::Any # Union{ {{/-first}}{{/anyOf}}{{#anyOf}}{{^-first}}, {{/-first}}{{{.}}}{{/anyOf}}{{#anyOf}}{{#-last}} } + {{classname}}() = new() + {{classname}}(value) = new(value) +end # type {{classname}}{{/-last}}{{/anyOf}} + +function OpenAPI.property_type(::Type{ {{classname}} }, name::Symbol, json::Dict{String,Any}) + {{#discriminator}}discriminator = json["{{propertyName}}"] + {{#hasDiscriminatorWithNonEmptyMapping}}{{#mappedModels}}{{#-first}}if{{/-first}}{{^-first}}elseif{{/-first}} discriminator == "{{mappingName}}" + return eval(Base.Meta.parse("{{modelName}}")) + {{#-last}}end{{/-last}}{{/mappedModels}}{{/hasDiscriminatorWithNonEmptyMapping}}{{^hasDiscriminatorWithNonEmptyMapping}}{{#anyOf}}{{#-first}}if{{/-first}}{{^-first}}elseif{{/-first}} discriminator == "{{.}}" + return eval(Base.Meta.parse("{{.}}")) + {{#-last}}end{{/-last}}{{/anyOf}}{{/hasDiscriminatorWithNonEmptyMapping}} + throw(OpenAPI.ValidationException("Invalid discriminator value: $discriminator for {{classname}}")){{/discriminator}}{{^discriminator}} + # no discriminator specified, can't determine the exact type + return fieldtype({{classname}}, name){{/discriminator}} +end \ No newline at end of file diff --git a/backend/src/api/spec/templates/partial_model_doc_anyof.mustache b/backend/src/api/spec/templates/partial_model_doc_anyof.mustache new file mode 100644 index 00000000..b5b5fabc --- /dev/null +++ b/backend/src/api/spec/templates/partial_model_doc_anyof.mustache @@ -0,0 +1,11 @@ +{{#anyOf}}{{#-first}} +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**value** | This is a anyOf model. The value must be any of the following types: {{/-first}}{{/anyOf}}{{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}} | {{description}} | {{^required}}[optional] {{/required}} + +{{#discriminator}}The discriminator field is `{{propertyName}}`{{#hasDiscriminatorWithNonEmptyMapping}} with the following mapping: +{{#mappedModels}} + - `{{mappingName}}`: `{{modelName}}` +{{/mappedModels}} +{{/hasDiscriminatorWithNonEmptyMapping}}{{/discriminator}} diff --git a/backend/src/api/spec/templates/partial_model_doc_oneof.mustache b/backend/src/api/spec/templates/partial_model_doc_oneof.mustache new file mode 100644 index 00000000..4d6d3fa6 --- /dev/null +++ b/backend/src/api/spec/templates/partial_model_doc_oneof.mustache @@ -0,0 +1,11 @@ +{{#oneOf}}{{#-first}} +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**value** | This is a oneOf model. The value must be exactly one of the following types: {{/-first}}{{/oneOf}}{{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}} | {{description}} | {{^required}}[optional] {{/required}} + +{{#discriminator}}The discriminator field is `{{propertyName}}`{{#hasDiscriminatorWithNonEmptyMapping}} with the following mapping: +{{#mappedModels}} + - `{{mappingName}}`: `{{modelName}}` +{{/mappedModels}} +{{/hasDiscriminatorWithNonEmptyMapping}}{{/discriminator}} \ No newline at end of file diff --git a/backend/src/api/spec/templates/partial_model_oneof.mustache b/backend/src/api/spec/templates/partial_model_oneof.mustache new file mode 100644 index 00000000..b75e4565 --- /dev/null +++ b/backend/src/api/spec/templates/partial_model_oneof.mustache @@ -0,0 +1,23 @@ +{{#oneOf}}{{#-first}} +@doc raw"""{{name}}{{#description}} +{{description}}{{/description}} + + {{classname}}(; value=nothing) +""" +mutable struct {{classname}} <: OpenAPI.OneOfAPIModel + value::Any # Union{ {{/-first}}{{/oneOf}}{{#oneOf}}{{^-first}}, {{/-first}}{{{.}}}{{/oneOf}}{{#oneOf}}{{#-last}} } + {{classname}}() = new() + {{classname}}(value) = new(value) +end # type {{classname}}{{/-last}}{{/oneOf}} + +function OpenAPI.property_type(::Type{ {{classname}} }, name::Symbol, json::Dict{String,Any}) + {{#discriminator}}discriminator = json["{{propertyName}}"] + {{#hasDiscriminatorWithNonEmptyMapping}}{{#mappedModels}}{{#-first}}if{{/-first}}{{^-first}}elseif{{/-first}} discriminator == "{{mappingName}}" + return eval(Base.Meta.parse("{{modelName}}")) + {{#-last}}end{{/-last}}{{/mappedModels}}{{/hasDiscriminatorWithNonEmptyMapping}}{{^hasDiscriminatorWithNonEmptyMapping}}{{#oneOf}}{{#-first}}if{{/-first}}{{^-first}}elseif{{/-first}} discriminator == "{{.}}" + return eval(Base.Meta.parse("{{.}}")) + {{#-last}}end{{/-last}}{{/oneOf}}{{/hasDiscriminatorWithNonEmptyMapping}} + throw(OpenAPI.ValidationException("Invalid discriminator value: $discriminator for {{classname}}")){{/discriminator}}{{^discriminator}} + # no discriminator specified, can't determine the exact type + return fieldtype({{classname}}, name){{/discriminator}} +end \ No newline at end of file diff --git a/backend/src/api/spec/templates/partial_model_single.mustache b/backend/src/api/spec/templates/partial_model_single.mustache new file mode 100644 index 00000000..0c9e7bd5 --- /dev/null +++ b/backend/src/api/spec/templates/partial_model_single.mustache @@ -0,0 +1,101 @@ +@doc raw"""{{name}}{{#description}} +{{description}}{{/description}} + + {{classname}}(; +{{#allVars}} + {{{name}}}={{#defaultValue}}{{{defaultValue}}}{{/defaultValue}}{{^defaultValue}}nothing{{/defaultValue}}, +{{/allVars}} + ) + +{{#allVars}} + - {{{name}}}::{{datatype}}{{#description}} : {{description}}{{/description}} +{{/allVars}} +""" +Base.@kwdef mutable struct {{classname}} <: OpenAPI.APIModel +{{#allVars}} + {{{name}}}{{#isPrimitiveType}}::Union{Nothing, {{{datatype}}}}{{/isPrimitiveType}}{{^isPrimitiveType}}{{#isContainer}}::Union{Nothing, {{#isMap}}Dict{{/isMap}}{{^isMap}}Vector{{/isMap}}}{{/isContainer}}{{/isPrimitiveType}} = {{#defaultValue}}{{{defaultValue}}}{{/defaultValue}}{{^defaultValue}}nothing{{/defaultValue}}{{^isPrimitiveType}} # spec type: Union{ Nothing, {{datatype}} }{{/isPrimitiveType}} +{{/allVars}} + + function {{classname}}( + {{#allVars}} + {{{name}}}{{#isPrimitiveType}}::Union{Nothing, {{{datatype}}}}{{/isPrimitiveType}}{{^isPrimitiveType}}{{#isContainer}}::Union{Nothing, {{#isMap}}Dict{{/isMap}}{{^isMap}}Vector{{/isMap}}}{{/isContainer}}{{/isPrimitiveType}}, + {{/allVars}}) + o = new({{#allVars}}{{{name}}}, {{/allVars}}) + OpenAPI.validate_properties(o) + return o + end +end # type {{classname}} + +StructTypes.StructType(::{{{classname}}}) = StructTypes.Struct() + +const _property_types_{{classname}} = Dict{Symbol,String}({{#allVars}}Symbol("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}")=>"{{datatype}}", {{/allVars}}) +OpenAPI.property_type(::Type{ {{classname}} }, name::Symbol) = Union{Nothing,eval(Base.Meta.parse(_property_types_{{classname}}[name]))} + +function OpenAPI.check_required(o::{{classname}}) +{{#allVars}} +{{#required}} + o.{{{name}}} === nothing && (return false) +{{/required}} +{{/allVars}} + true +end + +function OpenAPI.validate_properties(o::{{classname}}) + {{#allVars}} + OpenAPI.validate_property({{classname}}, Symbol("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"), o.{{{name}}}) + {{/allVars}} +end + +function OpenAPI.validate_property(::Type{ {{classname}} }, name::Symbol, val) +{{#allVars}} +{{#isEnum}}{{#allowableValues}} + if name === Symbol("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}") + OpenAPI.validate_param(name, "{{classname}}", :enum, val, [{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}]) + end +{{/allowableValues}}{{/isEnum}} +{{^isEnum}} +{{#format}} + if name === Symbol("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}") + OpenAPI.validate_param(name, "{{classname}}", :format, val, "{{format}}") + end +{{/format}} +{{#hasValidation}} + if name === Symbol("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}") +{{#maxLength}} + OpenAPI.validate_param(name, "{{classname}}", :maxLength, val, {{maxLength}}) +{{/maxLength}} +{{#minLength}} + OpenAPI.validate_param(name, "{{classname}}", :minLength, val, {{minLength}}) +{{/minLength}} +{{#maximum}} + OpenAPI.validate_param(name, "{{classname}}", :maximum, val, {{maximum}}, {{#exclusiveMaximum}}true{{/exclusiveMaximum}}{{^exclusiveMaximum}}false{{/exclusiveMaximum}}) +{{/maximum}} +{{#minimum}} + OpenAPI.validate_param(name, "{{classname}}", :minimum, val, {{minimum}}, {{#exclusiveMinimum}}true{{/exclusiveMinimum}}{{^exclusiveMinimum}}false{{/exclusiveMinimum}}) +{{/minimum}} +{{#maxItems}} + OpenAPI.validate_param(name, "{{classname}}", :maxItems, val, {{maxItems}}) +{{/maxItems}} +{{#minItems}} + OpenAPI.validate_param(name, "{{classname}}", :minItems, val, {{minItems}}) +{{/minItems}} +{{#uniqueItems}} + OpenAPI.validate_param(name, "{{classname}}", :uniqueItems, val, {{uniqueItems}}) +{{/uniqueItems}} +{{#maxProperties}} + OpenAPI.validate_param(name, "{{classname}}", :maxProperties, val, {{maxProperties}}) +{{/maxProperties}} +{{#minProperties}} + OpenAPI.validate_param(name, "{{classname}}", :minProperties, val, {{minProperties}}) +{{/minProperties}} +{{#pattern}} + OpenAPI.validate_param(name, "{{classname}}", :pattern, val, r"{{{pattern}}}") +{{/pattern}} +{{#multipleOf}} + OpenAPI.validate_param(name, "{{classname}}", :multipleOf, val, {{multipleOf}}) +{{/multipleOf}} + end +{{/hasValidation}} +{{/isEnum}} +{{/allVars}} +end \ No newline at end of file diff --git a/backend/src/api/spec/templates/server.mustache b/backend/src/api/spec/templates/server.mustache new file mode 100644 index 00000000..7aa166c5 --- /dev/null +++ b/backend/src/api/spec/templates/server.mustache @@ -0,0 +1,660 @@ +{{>partial_header}} + +@doc raw""" +Encapsulates generated server code for {{packageName}} + +The following server methods must be implemented: + +{{#apiInfo}} +{{#apis}} +{{#operations}} +{{#operation}} +- **{{operationId}}** + - *invocation:* {{httpMethod}} {{{path}}} + - *signature:* {{operationId}}(req::HTTP.Request{{#allParams}}{{#required}}, {{paramName}}::{{#isBinary}}Vector{UInt8}{{/isBinary}}{{^isBinary}}{{dataType}}{{/isBinary}}{{/required}}{{/allParams}};{{#allParams}}{{^required}} {{paramName}}=nothing,{{/required}}{{/allParams}}) -> {{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}Nothing{{/returnType}} +{{/operation}} +{{/operations}} +{{/apis}} +{{/apiInfo}} +""" +module {{packageName}} + + +using JSON3 +using HTTP +using URIs +using OpenAPI +using Oxygen +using OpenAPI.Servers +using Dates +using TimeZones +using Oxygen +using Oxygen.Reflection +using StructTypes +using Oxygen.Errors +import Oxygen: format_response!, handlerequest + +const API_VERSION = "{{appVersion}}" +Oxygen.Core.oxygen_title=""" + -== -- --- -- + =@@@@@@@@@. -@@@= =@@@= - - @@#- - #@@@@@@@@@@@# - + #@@# -@@@= @@@ @@@@- -- -=#=- =@@@@@@@###@@@@@@@= + #@@# -@@@= -.- - == ###=- - #@#- - =@@@@# - - #@@@@@ + #@@# -@@@= #@@@@ -- -@@@@= - -#@@ @@@@#- @@#= + #@@# =##=- =##= @@@= =## =#@@@@## =#= -=##=- ===- =#=- -=#= @@@@#- - + #@@# =@@#- #@@# @@@= #@@ #@@@@#==#@@@@# - - @@@@# - #@@@# #@@@@# - + #@@# =@@#- #@@# @@@= #@@ @@@=- =@@@ #@@@#- == -#@@# - ###= -=@@@@@@@##=== - + #@@# =@@#- #@@# @@@= #@@ ### #@@# @@@@@ - @@@@= - @@@- - =#@@@@@@@@@@@@# - + #@@# =@@#- #@@# @@@= #@@ =======#@@# ===--#@@@@= - -#@@@# - - ====#@@@@@@ + #@@# =@@#- #@@# @@@= #@@ =#@@@@@@@@@@@@@@# - =#= #@@@@=- == --#@@@= - - @@@@@ + #@@# .@@#- -#@@# @@@= #@@ =@@@= -------- @@# -=@@@= - =@@@@@- #@@@@ - = =@@@@= + @@@ @@@= =@@@# @@@= #@@ @@@ @@# -=@@# ###= #@@@=- ###= #@@=- #@@@@=- #@@@@ + -#@@@ @@@= -=@@@@# @@@= #@@ #@@#- -=@@@# -=- @@@@@@ - @@@@@= -- #@@@@@#= =#@@@@@= + - =#@@@@=. #@@@#= =#@@@@@@# @@@= #@@ -#@@@# -=@@@@@ -=@@@# #@@#=- ### - #@@@@@@@@@@@@@@@# + -@@@#=. #@@@@@@@# #@@= ##@= #@@ =#@@@@@@@= #@# --- @@@@@@ - ==########= - $(API_VERSION) + -- ----- ---- #@@# + -- +""" + +{{=<% %>=}} +""" +Stores API interface specifications including HTTP metadata, function signature and return type +""" +struct APISpec + http_method::String + path::String + handler::Function + return_type::Type +end + +""" +Global registry for API specifications +Structure: Dict{Symbol => Vector{APISpec}} +""" +const INTERFACE_SPECS = Dict{Symbol, Vector{APISpec}}() + +""" +Macro for defining interface specifications +""" +macro interface(api_group, http_method, path, func_def) + # Extract function name and return type + func_expr = func_def.args[1] + + # Get function name and return type + if func_expr.head == :(::) # Function definition with return type + call_expr = func_expr.args[1] + func_name = call_expr.args[1] + return_type = func_expr.args[2] + else # Function definition without return type + func_name = func_expr.args[1] + return_type = :Any # Use Any as default return type + end + + # Convert function definition to actual function + func = eval(quote + $(func_def) + end) + + # Register interface specification + quote + if !haskey(INTERFACE_SPECS, $(esc(api_group))) + INTERFACE_SPECS[$(esc(api_group))] = [] + end + push!(INTERFACE_SPECS[$(esc(api_group))], + APISpec($http_method, $path, $func, $return_type)) + nothing + end +end + +""" +Global registry for generated handler functions +Structure: Dict{Symbol => Dict{Symbol => Dict{Symbol => Function}}} +- First level: API group name (e.g., :TestApi) +- Second level: Operation name (e.g., :test_hello) +- Third level: Handler function type (:read, :validate, :invoke) +""" +const GENERATED_HANDLERS = Dict{Symbol, Dict{Symbol, Dict{Symbol, Function}}}() + + +""" +Parameters: +- api_group::Symbol: API group name (e.g., :TestApi) +- operation_name::Symbol: Operation name (e.g., :test_hello) +- handler_type::Symbol: Handler function type (:read, :validate, :invoke) +- handler_func::Function: Handler function +""" +macro generated_handler(api_group, op_name, h_type, handler_func) + # Convert function definition to actual function + func = eval(quote + $(handler_func) + end) + + quote + @assert $(esc(h_type)) in (:read, :validate, :invoke) "Handler type must be :read, :validate or :invoke" + if !haskey(GENERATED_HANDLERS, $(esc(api_group))) + GENERATED_HANDLERS[$(esc(api_group))] = Dict{Symbol, Dict{Symbol, Function}}() + end + if !haskey(GENERATED_HANDLERS[$(esc(api_group))], $(esc(op_name))) + GENERATED_HANDLERS[$(esc(api_group))][$(esc(op_name))] = Dict{Symbol, Function}() + end + GENERATED_HANDLERS[$(esc(api_group))][$(esc(op_name))][$(esc(h_type))] = $func + nothing + end +end + +""" +Get all handler functions for a specific operation + +Parameters: +- api_group::Symbol: API group name +- operation_name::Symbol: Operation name + +Returns: +- NamedTuple: (read=Function, validate=Function, invoke=Function) or nothing +""" +function get_operation_handlers(api_group::Symbol, operation_name::Symbol) + if !haskey(GENERATED_HANDLERS, api_group) + @warn "API group not found: $api_group" + return nothing + end + + if !haskey(GENERATED_HANDLERS[api_group], operation_name) + @warn "Operation not found: $api_group.$operation_name" + return nothing + end + + handlers = GENERATED_HANDLERS[api_group][operation_name] + + # Check if all required handler functions are present + required_types = [:read, :validate, :invoke] + for handler_type in required_types + if !haskey(handlers, handler_type) + @warn "Missing handler function: $api_group.$operation_name.$handler_type" + return nothing + end + end + + return ( + read = handlers[:read], + validate = handlers[:validate], + invoke = handlers[:invoke] + ) +end +<%={{ }}=%> + +include("modelincludes.jl") +{{#apiInfo}}{{#apis}} +include("apis/api_{{classname}}.jl"){{/apis}}{{/apiInfo}} + +""" +Register handlers for all APIs in this module in the supplied `Router` instance. + +Paramerets: +- `router`: Router to register handlers in +- `impl`: module that implements the server methods + +Optional parameters: +- `path_prefix`: prefix to be applied to all paths +- `optional_middlewares`: Register one or more optional middlewares to be applied to all requests. + +Optional middlewares can be one or more of: + - `init`: called before the request is processed + - `pre_validation`: called after the request is parsed but before validation + - `pre_invoke`: called after validation but before the handler is invoked + - `post_invoke`: called after the handler is invoked but before the response is sent + +The order in which middlewares are invoked are: +`init |> read |> pre_validation |> validate |> pre_invoke |> invoke |> post_invoke` +""" +function register(router::HTTP.Router, impl; path_prefix::String="", optional_middlewares...) + {{#apiInfo}} + {{#apis}} + register{{classname}}(router, impl; path_prefix=path_prefix, optional_middlewares...) + {{/apis}} + {{/apiInfo}} + return router +end + +{{=<% %>=}} + +""" +Automatically build OpenAPI middleware chain. + +Arguments: +- api_group::Symbol: Name of the API group +- operation_name::Symbol: Name of the operation +- impl: Implementation object +- optional_middlewares...: Optional extra middlewares + +Returns: +- Function: Composed middleware function +""" +function build_openapi_middleware(api_group::Symbol, operation_name::Symbol, impl; optional_middlewares...) + handlers = get_operation_handlers(api_group, operation_name) + + if isnothing(handlers) + error("Failed to build middleware chain: missing necessary handler for $api_group.$operation_name") + end + + @info "🔧 Building middleware chain: $api_group.$operation_name" + @info " - Read function: $(nameof(handlers.read))" + @info " - Validate function: $(nameof(handlers.validate))" + @info " - Invoke function: $(nameof(handlers.invoke))" + + # Build the middleware chain using OpenAPI.Servers.middleware + return OpenAPI.Servers.middleware( + impl, + handlers.read, + handlers.validate, + handlers.invoke; + optional_middlewares... + ) +end + + +""" +Check whether two types are compatible. +""" +function is_type_compatible(required_type::Type, actual_type::Type) + # If required type is Any, all types are compatible + required_type === Any && return true + + # If actual type is a subtype of required type, it's compatible + actual_type <: required_type && return true + + return false +end + +""" +Stores the result of function implementation check. +""" +struct ImplementationCheckResult + success::Bool # Whether the check passed + matching_spec::Union{APISpec, Nothing} # Matched spec + error_message::Union{String, Nothing} # Error message (if any) + impl_func::Function # Implementation function + impl_return_type::Type # Return type of implementation +end + +""" +Check whether a single function implementation conforms to spec. + +Returns: +- ImplementationCheckResult: Detailed check result +""" +function check_single_implementation(api_group::Symbol, impl_func::Function, impl_return_type::Type) + if !haskey(INTERFACE_SPECS, api_group) + return ImplementationCheckResult( + false, + nothing, + "No API spec found for group $api_group", + impl_func, + impl_return_type + ) + end + + specs = INTERFACE_SPECS[api_group] + matching_spec = nothing + + # Find matching spec + for spec in specs + if nameof(spec.handler) == nameof(impl_func) + matching_spec = spec + break + end + end + + if isnothing(matching_spec) + return ImplementationCheckResult( + false, + nothing, + "No API spec definition found for function $(nameof(impl_func))", + impl_func, + impl_return_type + ) + end + + # Validate parameters + @info "🔍 Validating parameters..." + spec_func_details = Oxygen.Core.parse_func_params(matching_spec.path, matching_spec.handler) + handler_func_details = Oxygen.Core.parse_func_params(matching_spec.path, impl_func) + + # Only check basic structure + if handler_func_details.info.name != spec_func_details.info.name + return ImplementationCheckResult( + false, + matching_spec, + "Function name mismatch", + impl_func, + impl_return_type + ) + end + + if length(handler_func_details.info.sig) != length(spec_func_details.info.sig) + return ImplementationCheckResult( + false, + matching_spec, + "Parameter count mismatch", + impl_func, + impl_return_type + ) + end + + # Check return type + spec_return_type = matching_spec.return_type + if !is_type_compatible(spec_return_type, impl_return_type) + return ImplementationCheckResult( + false, + matching_spec, + "Incompatible return type: expected $(spec_return_type), got $(impl_return_type)", + impl_func, + impl_return_type + ) + end + + return ImplementationCheckResult( + true, + matching_spec, + nothing, + impl_func, + impl_return_type + ) +end + +""" +Parse function definition and extract name and return type. + +Arguments: +- func_def: Function definition expression + +Returns: +- (func_name, return_type): Tuple containing function name and return type +""" +function parse_function_definition(func_def) + func_expr = func_def.args[1] + + # Extract function name and return type + if func_expr.head == :(::) # Function with return type annotation + call_expr = func_expr.args[1] + func_name = call_expr.args[1] + return_type = func_expr.args[2] + else # Function without return type + func_name = func_expr.args[1] + return_type = :Any # Default to Any + end + + return func_name, return_type +end + +""" +Parse registration configuration parameters. +""" +function parse_register_config(config) + default_config = ( + prefix = "", + tags = String[], + middleware = Function[], + interval = nothing, + cron = nothing + ) + + if config isa String || config isa Symbol + return merge(default_config, (prefix = config,)) + elseif Meta.isexpr(config, :tuple) + return merge( + default_config, + ( + prefix = config.args[1], + tags = length(config.args) > 1 ? config.args[2] : String[], + middleware = length(config.args) > 2 ? config.args[3] : Function[], + interval = length(config.args) > 3 ? config.args[4] : nothing, + cron = length(config.args) > 4 ? config.args[5] : nothing + ) + ) + else + error("Invalid config parameter format") + end +end + +""" +Serialize handler with error formatting and HTTP response wrapping. +""" +function serializer_handler(handler) + function serializer_with_http_response_handler(req::HTTP.Request) + return handlerequest(true; show_errors=true) do + try + format_response!(req, handler(req)) # Format and save response + return req.response + catch e + if isa(e, OpenAPI.ValidationException) + return HTTP.Response(422, + ["Content-Type" => "application/json"], + body=JSON3.write(Dict( + "error" => "Unprocessable Entity", + "message" => e.reason, + "details" => Dict( + "loc" => e.parameter, + "msg" => e.reason, + "type" => e.rule, + "input" => e.value + ) + )) + ) + elseif isa(e, Oxygen.Errors.ValidationError) + return HTTP.Response(422, + ["Content-Type" => "application/json"], + body=JSON3.write(Dict( + "error" => "Unprocessable Entity", + "message" => e.msg, + "details" => e.cause + )) + ) + end + rethrow(e) + end + end + end +end + +""" +Register API handler to routing system + +Parameters: +- api_group::Symbol: API group name +- func::Function: handler function +- return_type::Type: return type of the function +- prefix::String: API path prefix + +Returns: +- Bool: whether registration was successful +""" +function register_api_route( + api_group::Symbol, + func, + return_type::Type; + prefix::String="", + tags::Vector{String}=Vector{String}(), + middleware::Union{Vector, Nothing}=nothing, + interval::Union{Real, Nothing}=nothing, + cron::Union{String, Nothing}=nothing + ) + @info "🚀 Starting API route registration: $api_group.$(nameof(func))" + + # Ensure prefix format is correct + prefix = normalize_path_prefix(prefix) + + # Ensure API group exists + if !haskey(INTERFACE_SPECS, api_group) + @error "❌ API group $api_group not found in interface specification" + error("API group $api_group not defined") + return false + end + + # Check implementation + result = check_single_implementation(api_group, func, return_type) + if !result.success + @error "❌ Implementation check failed: $(result.error_message)" + error("Function $(string(nameof(func))) does not conform to spec: $(result.error_message)") + return false + end + @info "✅ Function $(string(nameof(func))) passed implementation check" + + # Register route + if !isnothing(result.matching_spec) + route = prefix * result.matching_spec.path + httpmethod = result.matching_spec.http_method + operation_name = nameof(result.matching_spec.handler) + @info "🌐 Registering route: $httpmethod $route" + try + + actual_middleware = isnothing(middleware) ? Function[] : middleware + # TODO + # openapi_middleware = build_openapi_middleware(api_group, operation_name, func; actual_middleware...) + parsed_route = Oxygen.Core.parse_route(httpmethod, route) + func_details = Oxygen.Core.parse_func_params(route, func) + + # Generate OpenAPI documentation (if enabled) + if Oxygen.CONTEXT[].docs.enabled[] + generate_openapi_docs(parsed_route, httpmethod, func_details, return_type) + end + + # Register handler + Oxygen.Core.registerhandler( + Oxygen.CONTEXT[], + Oxygen.CONTEXT[].service.router, + httpmethod, + parsed_route, + func, #TODO + func_details + ) + + @info "✨ Route registered successfully: $httpmethod $route" + return true + catch e + @error "❌ Route registration failed" exception=(e, catch_backtrace()) + return false + end + end + return false +end + +""" +Normalize path prefix +""" +function normalize_path_prefix(prefix::String) + # Ensure prefix starts with a slash + if !startswith(prefix, "/") + prefix = "/" * prefix + end + + # Ensure prefix does not end with a slash + if endswith(prefix, "/") + prefix = prefix[1:end-1] + end + + return prefix +end + +""" +Generate OpenAPI documentation +""" +function generate_openapi_docs(route, httpmethod, func_details, return_type) + try + queryparams = func_details.queryparams + pathparams = func_details.pathparams + headers = func_details.headers + bodyparams = func_details.bodyargs + + @info "📚 Generating OpenAPI docs: $route" + + Oxygen.Core.registerschema( + Oxygen.CONTEXT[].docs, + route, + httpmethod, + pathparams, + queryparams, + headers, + bodyparams, + Vector([return_type]) + ) + catch error + @warn "Failed to generate OpenAPI docs: $route - $error" + end +end + +# Define forced parameters (not user-overridable) +const FORCED_PARAMS = Dict( + :serialize => false, # Force disable default serializer since we use custom serializer_handler +) + +""" + serve([ctx::ServerContext]; + middleware::Vector=[], + host::String="127.0.0.1", + port::Int=8080; + kwargs...) + +Wrapper for Oxygen.serve to ensure correct middleware order and serialization handling. + +Parameters: +- ctx: optional ServerContext +- middleware: user-defined middleware list +- host: server host +- port: server port +- kwargs: additional parameters passed to Oxygen.serve + +Config options: +- async::Bool=false: run server asynchronously +- parallel::Bool=false: process requests in parallel +- catch_errors::Bool=true: catch errors +- show_errors::Bool=true: show error details +- docs::Bool=true: enable API documentation +- metrics::Bool=true: enable metrics collection +- show_banner::Bool=true: show startup banner +- docs_path::String="/docs": API docs path +- schema_path::String="/schema": OpenAPI schema path +- external_url::Union{String,Nothing}=nothing: external access URL + +Returns: +- Oxygen.Server instance +""" +function serve(; + host::String="127.0.0.1", + port::Int=8080, + middleware::Vector=[], + kwargs...) + async = Base.get(kwargs, :async, false) + + try + # Ensure serializer_handler is always at the end of middleware chain + final_middleware = if isempty(middleware) + [serializer_handler] + else + [middleware..., serializer_handler] + end + merged_params = merge(FORCED_PARAMS, Dict(kwargs)) + return Oxygen.Core.serve( + Oxygen.CONTEXT[]; + middleware=final_middleware, + host=host, + port=port, + merged_params...) + finally + # Close server on exit if not running asynchronously + if !async + terminate() + # Only reset state on exit if running interactively + isinteractive() && resetstate() + end + end +end +<%={{ }}=%> + + +{{#exportModels}} +# export models +{{#models}}{{#model}}export {{classname}} +{{/model}}{{/models}} +{{/exportModels}} +export @interface,@generated_handler,serve +end # module {{packageName}} diff --git a/generate-server.sh b/generate-server.sh index b6908c8a..8f1dde27 100755 --- a/generate-server.sh +++ b/generate-server.sh @@ -11,6 +11,7 @@ fi java -jar $JARNAME generate \ -i ./backend/src/api/spec/api-spec.yaml \ + -t ./backend/src/api/spec/templates/ \ -g julia-server \ -o ./backend/src/api/server \ --additional-properties=packageName=JuliaOSServer \