synor/sdk/ruby/lib/synor_database/client.rb
Gulshan Yadav a874faef13 feat: complete Phase 3 SDKs for Swift, C, C++, C#, and Ruby
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.
2026-01-27 02:23:07 +05:30

206 lines
5.8 KiB
Ruby

# frozen_string_literal: true
require "faraday"
require "json"
require "cgi"
module SynorDatabase
# Synor Database SDK Client
#
# @example
# client = SynorDatabase::Client.new(api_key: 'your-api-key')
#
# # Key-Value operations
# client.kv.set("key", "value")
# value = client.kv.get("key")
#
# # Document operations
# id = client.documents.create("users", { name: "Alice" })
# doc = client.documents.get("users", id)
#
class Client
attr_reader :config, :kv, :documents, :vectors, :timeseries
def initialize(api_key: nil, **options)
@config = Config.new(api_key: api_key, **options)
raise ArgumentError, "API key is required" unless @config.api_key
@conn = Faraday.new(url: @config.base_url) do |f|
f.request :json
f.response :json
f.options.timeout = @config.timeout
f.headers["Authorization"] = "Bearer #{@config.api_key}"
f.headers["X-SDK-Version"] = "ruby/#{VERSION}"
end
@closed = false
@kv = KeyValueStore.new(self)
@documents = DocumentStore.new(self)
@vectors = VectorStore.new(self)
@timeseries = TimeSeriesStore.new(self)
end
def closed?
@closed
end
def close
@closed = true
end
def health_check
response = request(:get, "/health")
response["status"] == "healthy"
rescue StandardError
false
end
# Key-Value Store
class KeyValueStore
def initialize(client)
@client = client
end
def get(key)
response = @client.request(:get, "/kv/#{CGI.escape(key)}")
response["value"]
end
def set(key, value, ttl: nil)
body = { key: key, value: value }
body[:ttl] = ttl if ttl
@client.request(:put, "/kv/#{CGI.escape(key)}", body)
end
def delete(key)
@client.request(:delete, "/kv/#{CGI.escape(key)}")
end
def list(prefix)
response = @client.request(:get, "/kv?prefix=#{CGI.escape(prefix)}")
(response["items"] || []).map { |h| KeyValue.from_hash(h) }
end
end
# Document Store
class DocumentStore
def initialize(client)
@client = client
end
def create(collection, document)
response = @client.request(:post, "/collections/#{CGI.escape(collection)}/documents", document)
response["id"]
end
def get(collection, id)
response = @client.request(:get, "/collections/#{CGI.escape(collection)}/documents/#{CGI.escape(id)}")
Document.from_hash(response)
end
def update(collection, id, update)
@client.request(:patch, "/collections/#{CGI.escape(collection)}/documents/#{CGI.escape(id)}", update)
end
def delete(collection, id)
@client.request(:delete, "/collections/#{CGI.escape(collection)}/documents/#{CGI.escape(id)}")
end
def query(collection, query)
body = query.to_h.compact
response = @client.request(:post, "/collections/#{CGI.escape(collection)}/query", body)
(response["documents"] || []).map { |h| Document.from_hash(h) }
end
end
# Vector Store
class VectorStore
def initialize(client)
@client = client
end
def upsert(collection, vectors)
entries = vectors.map { |v| v.is_a?(Hash) ? v : v.to_h }
@client.request(:post, "/vectors/#{CGI.escape(collection)}/upsert", { vectors: entries })
end
def search(collection, vector, k)
response = @client.request(:post, "/vectors/#{CGI.escape(collection)}/search", { vector: vector, k: k })
(response["results"] || []).map { |h| SearchResult.from_hash(h) }
end
def delete(collection, ids)
@client.request(:delete, "/vectors/#{CGI.escape(collection)}", { ids: ids })
end
end
# Time Series Store
class TimeSeriesStore
def initialize(client)
@client = client
end
def write(series, points)
entries = points.map { |p| p.is_a?(Hash) ? p : p.to_h }
@client.request(:post, "/timeseries/#{CGI.escape(series)}/write", { points: entries })
end
def query(series, range, aggregation: nil)
body = { range: range.to_h }
body[:aggregation] = aggregation.to_h if aggregation
response = @client.request(:post, "/timeseries/#{CGI.escape(series)}/query", body)
(response["points"] || []).map { |h| DataPoint.from_hash(h) }
end
end
# Internal request method
def request(method, path, body = nil)
check_closed!
with_retry do
response = case method
when :get then @conn.get(path)
when :post then @conn.post(path, body)
when :put then @conn.put(path, body)
when :patch then @conn.patch(path, body)
when :delete
if body
@conn.delete(path) { |req| req.body = body.to_json }
else
@conn.delete(path)
end
end
raise_on_error(response) unless response.success?
response.body || {}
end
end
private
def check_closed!
raise ClientClosedError, "Client has been closed" if @closed
end
def with_retry
last_error = nil
@config.retries.times do |attempt|
begin
return yield
rescue StandardError => e
last_error = e
puts "Attempt #{attempt + 1} failed: #{e.message}" if @config.debug
sleep(2**attempt) if attempt < @config.retries - 1
end
end
raise last_error
end
def raise_on_error(response)
body = response.body || {}
message = body["message"] || "HTTP #{response.status}"
code = body["code"]
raise HttpError.new(message, status_code: response.status, code: code)
end
end
end