Implements Database, Hosting, and Bridge SDKs for remaining languages: Swift SDKs: - SynorDatabase with KV, Document, Vector, TimeSeries stores - SynorHosting with domain, DNS, deployment, SSL operations - SynorBridge with lock-mint and burn-unlock cross-chain flows C SDKs: - database.h/c - multi-model database client - hosting.h/c - hosting and domain management - bridge.h/c - cross-chain asset transfers C++ SDKs: - database.hpp - modern C++17 with std::future async - hosting.hpp - domain and deployment operations - bridge.hpp - cross-chain bridge with wait operations C# SDKs: - SynorDatabase.cs - async/await with inner store classes - SynorHosting.cs - domain management and analytics - SynorBridge.cs - cross-chain with BridgeException handling Ruby SDKs: - synor_database - Struct-based types with Faraday HTTP - synor_hosting - domain, DNS, SSL, analytics - synor_bridge - lock-mint/burn-unlock with retry logic Phase 3 complete: Database/Hosting/Bridge now available in all 12 languages.
357 lines
10 KiB
Ruby
357 lines
10 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "faraday"
|
|
require "json"
|
|
require "uri"
|
|
|
|
module SynorHosting
|
|
# Synor Hosting SDK client for Ruby.
|
|
# Provides domain management, DNS, deployments, SSL, and analytics.
|
|
class Client
|
|
attr_reader :closed
|
|
|
|
def initialize(config)
|
|
@config = config
|
|
@closed = false
|
|
@conn = Faraday.new(url: config.endpoint) do |f|
|
|
f.request :json
|
|
f.response :json
|
|
f.options.timeout = config.timeout
|
|
f.headers["Authorization"] = "Bearer #{config.api_key}"
|
|
f.headers["Content-Type"] = "application/json"
|
|
f.headers["X-SDK-Version"] = "ruby/#{VERSION}"
|
|
end
|
|
end
|
|
|
|
# ==================== Domain Operations ====================
|
|
|
|
# Register a new domain
|
|
def register_domain(name, auto_renew: true)
|
|
body = { name: name, auto_renew: auto_renew }
|
|
response = post("/domains", body)
|
|
parse_domain(response)
|
|
end
|
|
|
|
# Get domain details
|
|
def get_domain(name)
|
|
response = get("/domains/#{encode(name)}")
|
|
parse_domain(response)
|
|
end
|
|
|
|
# Resolve domain to its record
|
|
def resolve_domain(name)
|
|
response = get("/domains/#{encode(name)}/resolve")
|
|
DomainRecord.new(
|
|
cid: response["cid"],
|
|
ttl: response["ttl"],
|
|
updated_at: response["updated_at"]
|
|
)
|
|
end
|
|
|
|
# Update domain record
|
|
def update_domain(name, cid:, ttl: nil)
|
|
body = { cid: cid }
|
|
body[:ttl] = ttl if ttl
|
|
response = put("/domains/#{encode(name)}", body)
|
|
parse_domain(response)
|
|
end
|
|
|
|
# Transfer domain ownership
|
|
def transfer_domain(name, new_owner)
|
|
body = { new_owner: new_owner }
|
|
response = post("/domains/#{encode(name)}/transfer", body)
|
|
parse_domain(response)
|
|
end
|
|
|
|
# List domains
|
|
def list_domains(limit: nil, offset: nil)
|
|
params = {}
|
|
params[:limit] = limit if limit
|
|
params[:offset] = offset if offset
|
|
response = get("/domains", params)
|
|
(response["domains"] || []).map { |d| parse_domain(d) }
|
|
end
|
|
|
|
# ==================== DNS Operations ====================
|
|
|
|
# Set DNS records for a domain
|
|
def set_dns_records(domain, records)
|
|
body = { records: records.map { |r| record_to_hash(r) } }
|
|
response = put("/domains/#{encode(domain)}/dns", body)
|
|
(response["records"] || []).map { |r| parse_dns_record(r) }
|
|
end
|
|
|
|
# Get DNS records for a domain
|
|
def get_dns_records(domain)
|
|
response = get("/domains/#{encode(domain)}/dns")
|
|
(response["records"] || []).map { |r| parse_dns_record(r) }
|
|
end
|
|
|
|
# Add a single DNS record
|
|
def add_dns_record(domain, record)
|
|
body = record_to_hash(record)
|
|
response = post("/domains/#{encode(domain)}/dns", body)
|
|
parse_dns_record(response)
|
|
end
|
|
|
|
# Delete a DNS record
|
|
def delete_dns_record(domain, record_type, name)
|
|
delete("/domains/#{encode(domain)}/dns/#{record_type}/#{encode(name)}")
|
|
nil
|
|
end
|
|
|
|
# ==================== Deployment Operations ====================
|
|
|
|
# Deploy content to a domain
|
|
def deploy(cid, domain, options = {})
|
|
body = { cid: cid, domain: domain }
|
|
body[:build_command] = options[:build_command] if options[:build_command]
|
|
body[:env_vars] = options[:env_vars] if options[:env_vars]
|
|
response = post("/deployments", body)
|
|
parse_deployment(response)
|
|
end
|
|
|
|
# Get deployment details
|
|
def get_deployment(id)
|
|
response = get("/deployments/#{encode(id)}")
|
|
parse_deployment(response)
|
|
end
|
|
|
|
# List deployments
|
|
def list_deployments(domain: nil, limit: nil, offset: nil)
|
|
params = {}
|
|
params[:domain] = domain if domain
|
|
params[:limit] = limit if limit
|
|
params[:offset] = offset if offset
|
|
response = get("/deployments", params)
|
|
(response["deployments"] || []).map { |d| parse_deployment(d) }
|
|
end
|
|
|
|
# Wait for deployment to complete
|
|
def wait_for_deployment(id, poll_interval: 5, max_wait: 600)
|
|
deadline = Time.now + max_wait
|
|
final_statuses = [DeploymentStatus::ACTIVE, DeploymentStatus::FAILED, DeploymentStatus::ROLLED_BACK]
|
|
|
|
while Time.now < deadline
|
|
deployment = get_deployment(id)
|
|
return deployment if final_statuses.include?(deployment.status)
|
|
sleep(poll_interval)
|
|
end
|
|
|
|
raise Error, "Timeout waiting for deployment"
|
|
end
|
|
|
|
# Rollback deployment
|
|
def rollback(domain, deployment_id)
|
|
body = { deployment_id: deployment_id }
|
|
response = post("/domains/#{encode(domain)}/rollback", body)
|
|
parse_deployment(response)
|
|
end
|
|
|
|
# ==================== SSL Operations ====================
|
|
|
|
# Provision SSL certificate for a domain
|
|
def provision_ssl(domain)
|
|
response = post("/domains/#{encode(domain)}/ssl", {})
|
|
parse_certificate(response)
|
|
end
|
|
|
|
# Get SSL certificate for a domain
|
|
def get_certificate(domain)
|
|
response = get("/domains/#{encode(domain)}/ssl")
|
|
parse_certificate(response)
|
|
end
|
|
|
|
# Renew SSL certificate
|
|
def renew_certificate(domain)
|
|
response = post("/domains/#{encode(domain)}/ssl/renew", {})
|
|
parse_certificate(response)
|
|
end
|
|
|
|
# ==================== Analytics Operations ====================
|
|
|
|
# Get analytics for a domain
|
|
def get_analytics(domain, start_time:, end_time:)
|
|
params = { start: start_time.to_i, end: end_time.to_i }
|
|
response = get("/domains/#{encode(domain)}/analytics", params)
|
|
parse_analytics(response)
|
|
end
|
|
|
|
# Get bandwidth stats
|
|
def get_bandwidth(domain, start_time:, end_time:)
|
|
params = { start: start_time.to_i, end: end_time.to_i }
|
|
response = get("/domains/#{encode(domain)}/analytics/bandwidth", params)
|
|
BandwidthStats.new(
|
|
total_bytes: response["total_bytes"],
|
|
cached_bytes: response["cached_bytes"],
|
|
uncached_bytes: response["uncached_bytes"]
|
|
)
|
|
end
|
|
|
|
# ==================== Lifecycle ====================
|
|
|
|
# Health check
|
|
def health_check
|
|
response = get("/health")
|
|
response["status"] == "healthy"
|
|
rescue StandardError
|
|
false
|
|
end
|
|
|
|
# Close the client
|
|
def close
|
|
@closed = true
|
|
@conn.close if @conn.respond_to?(:close)
|
|
end
|
|
|
|
private
|
|
|
|
def get(path, params = {})
|
|
execute { @conn.get(path, params).body }
|
|
end
|
|
|
|
def post(path, body)
|
|
execute { @conn.post(path, body).body }
|
|
end
|
|
|
|
def put(path, body)
|
|
execute { @conn.put(path, body).body }
|
|
end
|
|
|
|
def delete(path)
|
|
execute { @conn.delete(path).body }
|
|
end
|
|
|
|
def execute
|
|
raise ClientClosedError, "Client has been closed" if @closed
|
|
|
|
last_error = nil
|
|
@config.retries.times do |attempt|
|
|
begin
|
|
response = yield
|
|
check_error(response) if response.is_a?(Hash)
|
|
return response
|
|
rescue StandardError => e
|
|
last_error = e
|
|
sleep(2**attempt) if attempt < @config.retries - 1
|
|
end
|
|
end
|
|
raise last_error
|
|
end
|
|
|
|
def check_error(response)
|
|
return unless response["error"] || response["code"]
|
|
|
|
message = response["message"] || response["error"] || "Unknown error"
|
|
code = response["code"]
|
|
status = response["status_code"] || 0
|
|
raise HttpError.new(message, status_code: status, code: code)
|
|
end
|
|
|
|
def encode(str)
|
|
URI.encode_www_form_component(str)
|
|
end
|
|
|
|
def record_to_hash(record)
|
|
hash = {
|
|
type: record.type,
|
|
name: record.name,
|
|
value: record.value,
|
|
ttl: record.ttl
|
|
}
|
|
hash[:priority] = record.priority if record.priority
|
|
hash
|
|
end
|
|
|
|
def parse_domain(data)
|
|
return nil unless data
|
|
|
|
Domain.new(
|
|
name: data["name"],
|
|
owner: data["owner"],
|
|
status: data["status"],
|
|
record: data["record"] ? DomainRecord.new(
|
|
cid: data["record"]["cid"],
|
|
ttl: data["record"]["ttl"],
|
|
updated_at: data["record"]["updated_at"]
|
|
) : nil,
|
|
expires_at: data["expires_at"],
|
|
created_at: data["created_at"],
|
|
auto_renew: data["auto_renew"]
|
|
)
|
|
end
|
|
|
|
def parse_dns_record(data)
|
|
return nil unless data
|
|
|
|
DnsRecord.new(
|
|
type: data["type"],
|
|
name: data["name"],
|
|
value: data["value"],
|
|
ttl: data["ttl"],
|
|
priority: data["priority"]
|
|
)
|
|
end
|
|
|
|
def parse_deployment(data)
|
|
return nil unless data
|
|
|
|
Deployment.new(
|
|
id: data["id"],
|
|
domain: data["domain"],
|
|
cid: data["cid"],
|
|
status: data["status"],
|
|
url: data["url"],
|
|
created_at: data["created_at"],
|
|
deployed_at: data["deployed_at"],
|
|
build_logs: data["build_logs"],
|
|
error_message: data["error_message"]
|
|
)
|
|
end
|
|
|
|
def parse_certificate(data)
|
|
return nil unless data
|
|
|
|
Certificate.new(
|
|
domain: data["domain"],
|
|
status: data["status"],
|
|
issuer: data["issuer"],
|
|
issued_at: data["issued_at"],
|
|
expires_at: data["expires_at"],
|
|
auto_renew: data["auto_renew"],
|
|
fingerprint: data["fingerprint"]
|
|
)
|
|
end
|
|
|
|
def parse_analytics(data)
|
|
return nil unless data
|
|
|
|
Analytics.new(
|
|
domain: data["domain"],
|
|
time_range: AnalyticsTimeRange.new(
|
|
start_time: data["time_range"]&.dig("start"),
|
|
end_time: data["time_range"]&.dig("end")
|
|
),
|
|
requests: data["requests"] ? RequestStats.new(
|
|
total: data["requests"]["total"],
|
|
success: data["requests"]["success"],
|
|
error: data["requests"]["error"],
|
|
cached: data["requests"]["cached"]
|
|
) : nil,
|
|
bandwidth: data["bandwidth"] ? BandwidthStats.new(
|
|
total_bytes: data["bandwidth"]["total_bytes"],
|
|
cached_bytes: data["bandwidth"]["cached_bytes"],
|
|
uncached_bytes: data["bandwidth"]["uncached_bytes"]
|
|
) : nil,
|
|
visitors: data["visitors"],
|
|
page_views: data["page_views"],
|
|
top_paths: (data["top_paths"] || []).map do |p|
|
|
PathStats.new(path: p["path"], requests: p["requests"], bandwidth: p["bandwidth"])
|
|
end,
|
|
geo_distribution: (data["geo_distribution"] || []).map do |g|
|
|
GeoStats.new(country: g["country"], requests: g["requests"], bandwidth: g["bandwidth"])
|
|
end
|
|
)
|
|
end
|
|
end
|
|
end
|