diff --git a/CLAUDE.md b/CLAUDE.md index b378855..7136c7e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -139,13 +139,15 @@ When adding a new `SdkError` variant: `crates/node/src/lib.rs` uses `#[napi(constructor)]` and `#[napi(getter)]` macros. napi handles async conversion automatically. ### Ruby Binding Pattern -`crates/ruby/src/lib.rs` uses the `magnus` crate. All async SDK calls are wrapped via a single shared `tokio::runtime` (static `OnceLock`) using `.block_on()` to produce a synchronous Ruby API. Methods returning data return **JSON strings** — callers must parse with `JSON.parse()`. All parameters are passed as a single Ruby Hash with symbol keys (e.g. `get_endpoints(limit: 20)`). Required keys throw `ArgumentError` if missing; unknown keys also throw `ArgumentError` (validated via `validate_keys`). The magnus arity limit of 15 is why this pattern is used uniformly — all methods are registered with arity `1` (or `0` for zero-param methods). Classes are exposed under the `QuicknodeSdk` module and registered via `#[magnus::init(name = "quicknode_sdk")]`. +`crates/ruby/src/lib.rs` uses the `magnus` crate. All async SDK calls are wrapped via a single shared `tokio::runtime` (static `OnceLock`) using `.block_on()` to produce a synchronous Ruby API. Methods returning data return native Ruby `Hash`/`Array` (via `serde_magnus`); a per-client Ruby delegator in `ruby/lib/quicknode_sdk/clients/` wraps responses in `QuicknodeSdk::IndifferentHash` (a `Hash` subclass with `Hashie::Extensions::IndifferentAccess`) so callers can use either symbol or string keys. All parameters are passed as a single Ruby Hash with symbol keys (e.g. `get_endpoints(limit: 20)`). Required keys throw `ArgumentError` if missing; unknown keys also throw `ArgumentError` (validated via `validate_keys`). The magnus arity limit of 15 is why this pattern is used uniformly — all methods are registered with arity `1` (or `0` for zero-param methods). The native binding classes are registered under `QuicknodeSdk::Native::*` (via `#[magnus::init(name = "quicknode_sdk")]`); user-facing `QuicknodeSdk::SDK`/`Admin`/`Streams`/`Webhooks`/`KvStore` are pure-Ruby wrapper classes defined in `ruby/lib/quicknode_sdk/`. When adding a new Ruby method: 1. Accept `opts: RHash` as the single parameter 2. Call `validate_keys(&opts, &["key1", "key2", ...])?;` as the first line 3. Use `hash_require_string/i64/i32/bool/vec_string` for required fields and `hash_get_*` for optional fields -4. Register with `method!(ClassName::method_name, 1)` in the `init` function +4. Register with `method!(ClassName::method_name, 1)` in the `init` function on the `QuicknodeSdk::Native::*` class (the user-facing Ruby wrapper in `ruby/lib/quicknode_sdk/clients/` picks up new methods automatically via `method_missing`) +5. For methods returning data, the return type is `Result` and the call ends with `.and_then(to_ruby)`. The Ruby delegator wraps the result in `QuicknodeSdk::IndifferentHash` automatically — no per-method code is needed on the Ruby side. +6. Add a corresponding RBS signature to `ruby/sig/quicknode_sdk.rbs` under the matching client class. Method name must match step 4; keyword args must match the `validate_keys` list from step 2 with types derived from the `hash_require_*` / `hash_get_*` accessors (`hash_require_string` → `String`, `hash_get_string` → optional `?key: String`, `hash_*_i32`/`i64` → `Integer`, `hash_*_bool` → `bool`, `hash_*_vec_string` → `Array[String]`, `hash_get_map_string_string` → `Hash[String, String]`). Use `untyped` as the return type for methods that return `Result` (response is wrapped as a `Hashie::Mash` at the Ruby boundary) and `void` for methods returning `Result<(), Error>`. If a new exception class is added, also add it to the error hierarchy section at the top of the same file. ### Testing Core clients are tested using mocked API calls with wiremock. All functions making external http calls should be tested this way and test the happy path, errors, with params, and with bad params. Keep testing focused and flexible, avoid overtesting @@ -163,6 +165,7 @@ Core clients are tested using mocked API calls with wiremock. All functions maki - When updating `sdk.js` wrapper methods, verify the argument types match the underlying napi-rs constructor/method signature (object vs primitive) - When adding a new export to `sdk.js`, also add it to the named exports in `npm/sdk.mjs` — ESM named exports cannot be spread dynamically and must be listed explicitly - `python/sdk/__init__.pyi` is overwritten by `just python-build` — edit `init_manual_override.pyi` instead +- `ruby/sig/quicknode_sdk.rbs` is **manually maintained** — it is NOT auto-generated. It provides RBS type signatures so editor LSPs (VSCode Ruby LSP, Solargraph, RubyMine, Steep) autocomplete method names and keyword argument keys for `QuicknodeSdk::Admin/Streams/Webhooks/KvStore/DestinationAttributes` and the exception classes. Every change to method registration in `crates/ruby/src/lib.rs` (new method, renamed key, new arg, removed arg, type change) must be mirrored here in the same PR. Responses are typed as `untyped` because they're wrapped in `Hashie::Mash` at the Ruby boundary — that's intentional, do not try to type response shapes. - Always update examples alongside the code changes ### Security diff --git a/README.md b/README.md index 78762fb..3c07811 100644 --- a/README.md +++ b/README.md @@ -116,12 +116,11 @@ console.log(`${resp.data.length} endpoints`); ```ruby # Ruby -require "json" require "quicknode_sdk" qn = QuicknodeSdk::SDK.from_env -resp = JSON.parse(qn.admin.get_endpoints({})) -puts "#{resp["data"].length} endpoints" +resp = qn.admin.get_endpoints +puts "#{resp[:data].length} endpoints" ``` ## Configuration @@ -187,7 +186,7 @@ Each method below shows the call pattern in Rust, Python, Node.js, and Ruby in t - **Rust**: methods are `async` and return `Result`. Request structs use the [`bon`](https://docs.rs/bon) builder pattern via `::builder()`. - **Python**: methods are `async` — call with `await`. Parameters are kwargs; responses are native `pyclass` objects with attribute access. - **Node.js**: methods are `async` and take a single options object with camelCase keys. -- **Ruby**: methods are **blocking** (not async). Parameters are a single Hash with symbol keys. Responses that carry data are returned as **JSON strings** — wrap calls with `JSON.parse`. Unknown keys raise `ArgumentError`. +- **Ruby**: methods are **blocking** (not async). Parameters are a single Hash with symbol keys. Responses that carry data are returned as a `Hash` with indifferent access — `resp[:data]` and `resp["data"]` both work. Unknown parameter keys raise `ArgumentError`. --- @@ -231,7 +230,7 @@ const resp = await qn.admin.getEndpoints({ ```ruby # Ruby -resp = JSON.parse(qn.admin.get_endpoints(limit: 20, sort_by: "created_at", sort_direction: "desc")) +resp = qn.admin.get_endpoints(limit: 20, sort_by: "created_at", sort_direction: "desc") ``` ##### `create_endpoint` / `createEndpoint` @@ -263,7 +262,7 @@ const resp = await qn.admin.createEndpoint({ chain: "ethereum", network: "mainne ```ruby # Ruby -resp = JSON.parse(qn.admin.create_endpoint(chain: "ethereum", network: "mainnet")) +resp = qn.admin.create_endpoint(chain: "ethereum", network: "mainnet") ``` ##### `show_endpoint` / `showEndpoint` @@ -291,7 +290,7 @@ const resp = await qn.admin.showEndpoint("ep-123"); ```ruby # Ruby -resp = JSON.parse(qn.admin.show_endpoint(id: "ep-123")) +resp = qn.admin.show_endpoint(id: "ep-123") ``` ##### `update_endpoint` / `updateEndpoint` @@ -377,7 +376,7 @@ await qn.admin.updateEndpointStatus("ep-123", { status: "paused" }); ```ruby # Ruby -JSON.parse(qn.admin.update_endpoint_status(id: "ep-123", status: "paused")) +qn.admin.update_endpoint_status(id: "ep-123", status: "paused") ``` #### Endpoint Tags @@ -468,7 +467,7 @@ const resp = await qn.admin.listTeams(); ```ruby # Ruby -resp = JSON.parse(qn.admin.list_teams) +resp = qn.admin.list_teams ``` ##### `create_team` / `createTeam` @@ -497,7 +496,7 @@ const resp = await qn.admin.createTeam({ name: "Payments" }); ```ruby # Ruby -resp = JSON.parse(qn.admin.create_team(name: "Payments")) +resp = qn.admin.create_team(name: "Payments") ``` ##### `get_team` / `getTeam` @@ -525,7 +524,7 @@ const resp = await qn.admin.getTeam(42); ```ruby # Ruby -resp = JSON.parse(qn.admin.get_team(id: 42)) +resp = qn.admin.get_team(id: 42) ``` ##### `delete_team` / `deleteTeam` @@ -581,7 +580,7 @@ const resp = await qn.admin.listTeamEndpoints(42); ```ruby # Ruby -resp = JSON.parse(qn.admin.list_team_endpoints(id: 42)) +resp = qn.admin.list_team_endpoints(id: 42) ``` ##### `update_team_endpoints` / `updateTeamEndpoints` @@ -730,7 +729,7 @@ const resp = await qn.admin.getUsage(); ```ruby # Ruby -resp = JSON.parse(qn.admin.get_usage({})) +resp = qn.admin.get_usage({}) ``` ##### `get_usage_by_endpoint` / `getUsageByEndpoint` @@ -756,7 +755,7 @@ const resp = await qn.admin.getUsageByEndpoint(); ```ruby # Ruby -resp = JSON.parse(qn.admin.get_usage_by_endpoint({})) +resp = qn.admin.get_usage_by_endpoint({}) ``` ##### `get_usage_by_method` / `getUsageByMethod` @@ -782,7 +781,7 @@ const resp = await qn.admin.getUsageByMethod(); ```ruby # Ruby -resp = JSON.parse(qn.admin.get_usage_by_method({})) +resp = qn.admin.get_usage_by_method({}) ``` ##### `get_usage_by_chain` / `getUsageByChain` @@ -808,7 +807,7 @@ const resp = await qn.admin.getUsageByChain(); ```ruby # Ruby -resp = JSON.parse(qn.admin.get_usage_by_chain({})) +resp = qn.admin.get_usage_by_chain({}) ``` ##### `get_usage_by_tag` / `getUsageByTag` @@ -834,7 +833,7 @@ const resp = await qn.admin.getUsageByTag(); ```ruby # Ruby -resp = JSON.parse(qn.admin.get_usage_by_tag({})) +resp = qn.admin.get_usage_by_tag({}) ``` #### Logs @@ -878,12 +877,12 @@ const resp = await qn.admin.getEndpointLogs("ep-123", { ```ruby # Ruby -resp = JSON.parse(qn.admin.get_endpoint_logs( +resp = qn.admin.get_endpoint_logs( id: "ep-123", from_time: "2026-04-01T00:00:00Z", to_time: "2026-04-02T00:00:00Z", limit: 100 -)) +) ``` ##### `get_log_details` / `getLogDetails` @@ -911,7 +910,7 @@ const resp = await qn.admin.getLogDetails("ep-123", "req-abc"); ```ruby # Ruby -resp = JSON.parse(qn.admin.get_log_details(id: "ep-123", request_id: "req-abc")) +resp = qn.admin.get_log_details(id: "ep-123", request_id: "req-abc") ``` #### Endpoint Security @@ -941,7 +940,7 @@ const resp = await qn.admin.getEndpointSecurity("ep-123"); ```ruby # Ruby -resp = JSON.parse(qn.admin.get_endpoint_security(id: "ep-123")) +resp = qn.admin.get_endpoint_security(id: "ep-123") ``` #### Security Options @@ -971,7 +970,7 @@ const resp = await qn.admin.getSecurityOptions("ep-123"); ```ruby # Ruby -resp = JSON.parse(qn.admin.get_security_options(id: "ep-123")) +resp = qn.admin.get_security_options(id: "ep-123") ``` ##### `update_security_options` / `updateSecurityOptions` @@ -1186,7 +1185,7 @@ await qn.admin.deleteIp("ep-123", "ip-1"); ```ruby # Ruby -resp = JSON.parse(qn.admin.delete_ip(id: "ep-123", ip_id: "ip-1")) +resp = qn.admin.delete_ip(id: "ep-123", ip_id: "ip-1") ``` #### Domain Masks @@ -1362,10 +1361,10 @@ const resp = await qn.admin.createRequestFilter("ep-123", { ```ruby # Ruby -resp = JSON.parse(qn.admin.create_request_filter( +resp = qn.admin.create_request_filter( id: "ep-123", methods: ["eth_blockNumber", "eth_getBalance"] -)) +) ``` ##### `update_request_filter` / `updateRequestFilter` @@ -1511,10 +1510,10 @@ await qn.admin.createOrUpdateIpCustomHeader("ep-123", { headerName: "X-Forwarded ```ruby # Ruby -JSON.parse(qn.admin.create_or_update_ip_custom_header( +qn.admin.create_or_update_ip_custom_header( id: "ep-123", header_name: "X-Forwarded-For" -)) +) ``` ##### `delete_ip_custom_header` / `deleteIpCustomHeader` @@ -1542,7 +1541,7 @@ await qn.admin.deleteIpCustomHeader("ep-123"); ```ruby # Ruby -JSON.parse(qn.admin.delete_ip_custom_header(id: "ep-123")) +qn.admin.delete_ip_custom_header(id: "ep-123") ``` #### Method Rate Limits @@ -1572,7 +1571,7 @@ const resp = await qn.admin.getMethodRateLimits("ep-123"); ```ruby # Ruby -resp = JSON.parse(qn.admin.get_method_rate_limits(id: "ep-123")) +resp = qn.admin.get_method_rate_limits(id: "ep-123") ``` ##### `create_method_rate_limit` / `createMethodRateLimit` @@ -1614,12 +1613,12 @@ const resp = await qn.admin.createMethodRateLimit("ep-123", { ```ruby # Ruby -resp = JSON.parse(qn.admin.create_method_rate_limit( +resp = qn.admin.create_method_rate_limit( id: "ep-123", interval: "second", methods: ["eth_call"], rate: 10 -)) +) ``` ##### `update_method_rate_limit` / `updateMethodRateLimit` @@ -1648,7 +1647,7 @@ await qn.admin.updateMethodRateLimit("ep-123", "rl-1", { rate: 50 }); ```ruby # Ruby -JSON.parse(qn.admin.update_method_rate_limit(id: "ep-123", method_rate_limit_id: "rl-1", rate: 50)) +qn.admin.update_method_rate_limit(id: "ep-123", method_rate_limit_id: "rl-1", rate: 50) ``` ##### `delete_method_rate_limit` / `deleteMethodRateLimit` @@ -1749,11 +1748,11 @@ const resp = await qn.admin.getEndpointMetrics("ep-123", { ```ruby # Ruby -resp = JSON.parse(qn.admin.get_endpoint_metrics( +resp = qn.admin.get_endpoint_metrics( id: "ep-123", period: "day", metric: "method_calls_over_time" -)) +) ``` ##### `get_account_metrics` / `getAccountMetrics` @@ -1789,7 +1788,7 @@ const resp = await qn.admin.getAccountMetrics({ ```ruby # Ruby -resp = JSON.parse(qn.admin.get_account_metrics(period: "day", metric: "credits_over_time")) +resp = qn.admin.get_account_metrics(period: "day", metric: "credits_over_time") ``` #### Chains @@ -1819,7 +1818,7 @@ const resp = await qn.admin.listChains(); ```ruby # Ruby -resp = JSON.parse(qn.admin.list_chains) +resp = qn.admin.list_chains ``` #### Billing @@ -1849,7 +1848,7 @@ const resp = await qn.admin.listInvoices(); ```ruby # Ruby -resp = JSON.parse(qn.admin.list_invoices) +resp = qn.admin.list_invoices ``` ##### `list_payments` / `listPayments` @@ -1877,7 +1876,7 @@ const resp = await qn.admin.listPayments(); ```ruby # Ruby -resp = JSON.parse(qn.admin.list_payments) +resp = qn.admin.list_payments ``` #### Bulk Operations @@ -1914,7 +1913,7 @@ const resp = await qn.admin.bulkUpdateEndpointStatus({ ```ruby # Ruby -resp = JSON.parse(qn.admin.bulk_update_endpoint_status(ids: ["ep-1", "ep-2"], status: "paused")) +resp = qn.admin.bulk_update_endpoint_status(ids: ["ep-1", "ep-2"], status: "paused") ``` ##### `bulk_add_tag` / `bulkAddTag` @@ -1946,7 +1945,7 @@ const resp = await qn.admin.bulkAddTag({ ids: ["ep-1", "ep-2"], label: "prod" }) ```ruby # Ruby -resp = JSON.parse(qn.admin.bulk_add_tag(ids: ["ep-1", "ep-2"], label: "prod")) +resp = qn.admin.bulk_add_tag(ids: ["ep-1", "ep-2"], label: "prod") ``` ##### `bulk_remove_tag` / `bulkRemoveTag` @@ -1978,7 +1977,7 @@ const resp = await qn.admin.bulkRemoveTag({ ids: ["ep-1", "ep-2"], tagId: 42 }); ```ruby # Ruby -resp = JSON.parse(qn.admin.bulk_remove_tag(ids: ["ep-1", "ep-2"], tag_id: 42)) +resp = qn.admin.bulk_remove_tag(ids: ["ep-1", "ep-2"], tag_id: 42) ``` #### Account Tags @@ -2008,7 +2007,7 @@ const resp = await qn.admin.listTags(); ```ruby # Ruby -resp = JSON.parse(qn.admin.list_tags) +resp = qn.admin.list_tags ``` ##### `rename_tag` / `renameTag` @@ -2037,7 +2036,7 @@ const resp = await qn.admin.renameTag(42, { label: "staging" }); ```ruby # Ruby -resp = JSON.parse(qn.admin.rename_tag(tag_id: 42, label: "staging")) +resp = qn.admin.rename_tag(tag_id: 42, label: "staging") ``` ##### `delete_account_tag` / `deleteAccountTag` @@ -2065,7 +2064,7 @@ await qn.admin.deleteAccountTag(42); ```ruby # Ruby -JSON.parse(qn.admin.delete_account_tag(id: 42)) +qn.admin.delete_account_tag(id: 42) ``` --- @@ -2202,7 +2201,7 @@ dest = QuicknodeSdk::DestinationAttributes.webhook( post_timeout_sec: 10, compression: "none" ) -stream = JSON.parse(qn.streams.create_stream( +stream = qn.streams.create_stream( name: "My Stream", network: "ethereum-mainnet", dataset: "block", @@ -2213,7 +2212,7 @@ stream = JSON.parse(qn.streams.create_stream( plan: "growth_plan", threshold_fetch_buffer: 1000, status: "active" -)) +) ``` ##### `list_streams` / `listStreams` @@ -2241,7 +2240,7 @@ const resp = await qn.streams.listStreams(); ```ruby # Ruby -resp = JSON.parse(qn.streams.list_streams({})) +resp = qn.streams.list_streams({}) ``` ##### `get_stream` / `getStream` @@ -2269,7 +2268,7 @@ const stream = await qn.streams.getStream("stream-id"); ```ruby # Ruby -stream = JSON.parse(qn.streams.get_stream(id: "stream-id")) +stream = qn.streams.get_stream(id: "stream-id") ``` ##### `update_stream` / `updateStream` @@ -2301,7 +2300,7 @@ const stream = await qn.streams.updateStream("stream-id", { name: "Renamed" }); ```ruby # Ruby -stream = JSON.parse(qn.streams.update_stream(id: "stream-id", name: "Renamed")) +stream = qn.streams.update_stream(id: "stream-id", name: "Renamed") ``` ##### `delete_stream` / `deleteStream` @@ -2459,11 +2458,11 @@ const resp = await qn.streams.testFilter({ ```ruby # Ruby -resp = JSON.parse(qn.streams.test_filter( +resp = qn.streams.test_filter( network: "ethereum-mainnet", dataset: "block", block: "17811625" -)) +) ``` ##### `get_enabled_count` / `getEnabledCount` @@ -2491,7 +2490,7 @@ const resp = await qn.streams.getEnabledCount(); ```ruby # Ruby -resp = JSON.parse(qn.streams.get_enabled_count({})) +resp = qn.streams.get_enabled_count({}) ``` --- @@ -2561,7 +2560,7 @@ const resp = await qn.webhooks.listWebhooks(); ```ruby # Ruby -resp = JSON.parse(qn.webhooks.list_webhooks({})) +resp = qn.webhooks.list_webhooks({}) ``` ##### `get_webhook` / `getWebhook` @@ -2589,7 +2588,7 @@ const webhook = await qn.webhooks.getWebhook("wh-1"); ```ruby # Ruby -webhook = JSON.parse(qn.webhooks.get_webhook(id: "wh-1")) +webhook = qn.webhooks.get_webhook(id: "wh-1") ``` ##### `create_webhook_from_template` / `createWebhookFromTemplate` @@ -2657,12 +2656,12 @@ template_args = JSON.generate({ templateId: "evmWalletFilter", templateArgs: { wallets: ["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"] } }) -webhook = JSON.parse(qn.webhooks.create_webhook_from_template( +webhook = qn.webhooks.create_webhook_from_template( name: "Wallet Webhook", network: "ethereum-mainnet", destination_attributes_json: destination_attributes, template_args_json: template_args -)) +) ``` ##### `update_webhook` / `updateWebhook` @@ -2694,7 +2693,7 @@ const webhook = await qn.webhooks.updateWebhook("wh-1", { name: "Renamed Webhook ```ruby # Ruby -webhook = JSON.parse(qn.webhooks.update_webhook(id: "wh-1", name: "Renamed Webhook")) +webhook = qn.webhooks.update_webhook(id: "wh-1", name: "Renamed Webhook") ``` ##### `update_webhook_template` / `updateWebhookTemplate` @@ -2742,10 +2741,10 @@ template_args = JSON.generate({ templateId: "evmWalletFilter", templateArgs: { wallets: ["0xnewwallet"] } }) -webhook = JSON.parse(qn.webhooks.update_webhook_template( +webhook = qn.webhooks.update_webhook_template( webhook_id: "wh-1", template_args_json: template_args -)) +) ``` ##### `delete_webhook` / `deleteWebhook` @@ -2888,7 +2887,7 @@ const resp = await qn.webhooks.getEnabledCount(); ```ruby # Ruby -resp = JSON.parse(qn.webhooks.get_enabled_count) +resp = qn.webhooks.get_enabled_count ``` --- @@ -2955,7 +2954,7 @@ const resp = await qn.kvstore.getSets(); ```ruby # Ruby -resp = JSON.parse(qn.kvstore.get_sets({})) +resp = qn.kvstore.get_sets({}) ``` ##### `get_set` / `getSet` @@ -2983,7 +2982,7 @@ const resp = await qn.kvstore.getSet("my-key"); ```ruby # Ruby -resp = JSON.parse(qn.kvstore.get_set(key: "my-key")) +resp = qn.kvstore.get_set(key: "my-key") ``` ##### `bulk_sets` / `bulkSets` @@ -3113,7 +3112,7 @@ const resp = await qn.kvstore.getLists(); ```ruby # Ruby -resp = JSON.parse(qn.kvstore.get_lists({})) +resp = qn.kvstore.get_lists({}) ``` ##### `get_list` / `getList` @@ -3141,7 +3140,7 @@ const resp = await qn.kvstore.getList("my-list"); ```ruby # Ruby -resp = JSON.parse(qn.kvstore.get_list(key: "my-list")) +resp = qn.kvstore.get_list(key: "my-list") ``` ##### `update_list` / `updateList` @@ -3241,7 +3240,7 @@ const resp = await qn.kvstore.listContainsItem("my-list", "0x123"); ```ruby # Ruby -resp = JSON.parse(qn.kvstore.list_contains_item(key: "my-list", item: "0x123")) +resp = qn.kvstore.list_contains_item(key: "my-list", item: "0x123") ``` ##### `delete_list_item` / `deleteListItem` diff --git a/crates/ruby/Cargo.toml b/crates/ruby/Cargo.toml index 1a7eea3..39024ba 100644 --- a/crates/ruby/Cargo.toml +++ b/crates/ruby/Cargo.toml @@ -19,6 +19,7 @@ magnus = { workspace = true } tokio = { version = "1", features = ["rt-multi-thread"] } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } +serde_magnus = "0.11" [build-dependencies] rb-sys-build = "0.9" diff --git a/crates/ruby/src/lib.rs b/crates/ruby/src/lib.rs index efc9fe7..ccf7fa0 100644 --- a/crates/ruby/src/lib.rs +++ b/crates/ruby/src/lib.rs @@ -32,8 +32,13 @@ fn parse_err(e: serde_json::Error) -> Error { Error::new(ruby().exception_arg_error(), e.to_string()) } -fn to_json(v: T) -> Result { - serde_json::to_string(&v).map_err(parse_err) +fn to_ruby(v: T) -> Result { + serde_magnus::serialize(&ruby(), &v).map_err(|e| { + Error::new( + ruby().exception_runtime_error(), + format!("response serialization failed: {e}"), + ) + }) } fn parse_enum(s: String) -> Result { @@ -222,7 +227,7 @@ fn hash_get_extra_destinations( // ── QuicknodeSdk ──────────────────────────────────────────────────────────── -#[magnus::wrap(class = "QuicknodeSdk::SDK", free_immediately, size)] +#[magnus::wrap(class = "QuicknodeSdk::Native::SDK", free_immediately, size)] pub struct QuicknodeSdk { inner: core::QuicknodeSdk, } @@ -261,9 +266,11 @@ impl QuicknodeSdk { // ── AdminApiClient ────────────────────────────────────────────────────────── // -// All methods return JSON strings. Call JSON.parse on the result in Ruby. +// Methods returning data return native Ruby Hash/Array via serde_magnus. The +// Ruby package wraps these in QuicknodeSdk::IndifferentHash (a Hash subclass +// with Hashie::Extensions::IndifferentAccess) before returning to the user. -#[magnus::wrap(class = "QuicknodeSdk::Admin", free_immediately, size)] +#[magnus::wrap(class = "QuicknodeSdk::Native::Admin", free_immediately, size)] #[derive(Clone)] pub struct AdminApiClient { inner: core::admin::AdminApiClient, @@ -271,7 +278,7 @@ pub struct AdminApiClient { #[allow(clippy::needless_pass_by_value)] impl AdminApiClient { - fn get_endpoints(&self, opts: RHash) -> Result { + fn get_endpoints(&self, opts: RHash) -> Result { validate_keys( &opts, &[ @@ -307,10 +314,10 @@ impl AdminApiClient { runtime() .block_on(client.get_endpoints(¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn create_endpoint(&self, opts: RHash) -> Result { + fn create_endpoint(&self, opts: RHash) -> Result { validate_keys(&opts, &["chain", "network"])?; let client = self.inner.clone(); let params = core::admin::CreateEndpointRequest { @@ -320,17 +327,17 @@ impl AdminApiClient { runtime() .block_on(client.create_endpoint(¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn show_endpoint(&self, opts: RHash) -> Result { + fn show_endpoint(&self, opts: RHash) -> Result { validate_keys(&opts, &["id"])?; let client = self.inner.clone(); let id = hash_require_string(&opts, "id")?; runtime() .block_on(client.show_endpoint(&id)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } fn update_endpoint(&self, opts: RHash) -> Result<(), Error> { @@ -354,7 +361,7 @@ impl AdminApiClient { .map_err(map_err) } - fn update_endpoint_status(&self, opts: RHash) -> Result { + fn update_endpoint_status(&self, opts: RHash) -> Result { validate_keys(&opts, &["id", "status"])?; let client = self.inner.clone(); let id = hash_require_string(&opts, "id")?; @@ -364,7 +371,7 @@ impl AdminApiClient { runtime() .block_on(client.update_endpoint_status(&id, ¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } fn create_tag(&self, opts: RHash) -> Result<(), Error> { @@ -389,7 +396,7 @@ impl AdminApiClient { .map_err(map_err) } - fn get_usage(&self, opts: RHash) -> Result { + fn get_usage(&self, opts: RHash) -> Result { validate_keys(&opts, &["start_time", "end_time"])?; let client = self.inner.clone(); let params = core::admin::GetUsageRequest { @@ -399,10 +406,10 @@ impl AdminApiClient { runtime() .block_on(client.get_usage(¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn get_usage_by_endpoint(&self, opts: RHash) -> Result { + fn get_usage_by_endpoint(&self, opts: RHash) -> Result { validate_keys(&opts, &["start_time", "end_time"])?; let client = self.inner.clone(); let params = core::admin::GetUsageRequest { @@ -412,10 +419,10 @@ impl AdminApiClient { runtime() .block_on(client.get_usage_by_endpoint(¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn get_usage_by_method(&self, opts: RHash) -> Result { + fn get_usage_by_method(&self, opts: RHash) -> Result { validate_keys(&opts, &["start_time", "end_time"])?; let client = self.inner.clone(); let params = core::admin::GetUsageRequest { @@ -425,10 +432,10 @@ impl AdminApiClient { runtime() .block_on(client.get_usage_by_method(¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn get_usage_by_chain(&self, opts: RHash) -> Result { + fn get_usage_by_chain(&self, opts: RHash) -> Result { validate_keys(&opts, &["start_time", "end_time"])?; let client = self.inner.clone(); let params = core::admin::GetUsageRequest { @@ -438,10 +445,10 @@ impl AdminApiClient { runtime() .block_on(client.get_usage_by_chain(¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn get_endpoint_logs(&self, opts: RHash) -> Result { + fn get_endpoint_logs(&self, opts: RHash) -> Result { validate_keys( &opts, &[ @@ -465,10 +472,10 @@ impl AdminApiClient { runtime() .block_on(client.get_endpoint_logs(&id, ¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn get_log_details(&self, opts: RHash) -> Result { + fn get_log_details(&self, opts: RHash) -> Result { validate_keys(&opts, &["id", "request_id"])?; let client = self.inner.clone(); let id = hash_require_string(&opts, "id")?; @@ -476,20 +483,20 @@ impl AdminApiClient { runtime() .block_on(client.get_log_details(&id, &request_id)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn get_security_options(&self, opts: RHash) -> Result { + fn get_security_options(&self, opts: RHash) -> Result { validate_keys(&opts, &["id"])?; let client = self.inner.clone(); let id = hash_require_string(&opts, "id")?; runtime() .block_on(client.get_security_options(&id)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn update_security_options(&self, opts: RHash) -> Result { + fn update_security_options(&self, opts: RHash) -> Result { validate_keys( &opts, &[ @@ -523,7 +530,7 @@ impl AdminApiClient { runtime() .block_on(client.update_security_options(&id, ¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } fn create_token(&self, opts: RHash) -> Result<(), Error> { @@ -535,7 +542,7 @@ impl AdminApiClient { .map_err(map_err) } - fn delete_token(&self, opts: RHash) -> Result { + fn delete_token(&self, opts: RHash) -> Result { validate_keys(&opts, &["id", "token_id"])?; let client = self.inner.clone(); let id = hash_require_string(&opts, "id")?; @@ -543,7 +550,7 @@ impl AdminApiClient { runtime() .block_on(client.delete_token(&id, &token_id)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } fn create_referrer(&self, opts: RHash) -> Result<(), Error> { @@ -558,7 +565,7 @@ impl AdminApiClient { .map_err(map_err) } - fn delete_referrer(&self, opts: RHash) -> Result { + fn delete_referrer(&self, opts: RHash) -> Result { validate_keys(&opts, &["id", "referrer_id"])?; let client = self.inner.clone(); let id = hash_require_string(&opts, "id")?; @@ -566,7 +573,7 @@ impl AdminApiClient { runtime() .block_on(client.delete_referrer(&id, &referrer_id)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } fn create_ip(&self, opts: RHash) -> Result<(), Error> { @@ -581,7 +588,7 @@ impl AdminApiClient { .map_err(map_err) } - fn delete_ip(&self, opts: RHash) -> Result { + fn delete_ip(&self, opts: RHash) -> Result { validate_keys(&opts, &["id", "ip_id"])?; let client = self.inner.clone(); let id = hash_require_string(&opts, "id")?; @@ -589,7 +596,7 @@ impl AdminApiClient { runtime() .block_on(client.delete_ip(&id, &ip_id)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } fn create_domain_mask(&self, opts: RHash) -> Result<(), Error> { @@ -604,7 +611,7 @@ impl AdminApiClient { .map_err(map_err) } - fn delete_domain_mask(&self, opts: RHash) -> Result { + fn delete_domain_mask(&self, opts: RHash) -> Result { validate_keys(&opts, &["id", "domain_mask_id"])?; let client = self.inner.clone(); let id = hash_require_string(&opts, "id")?; @@ -612,7 +619,7 @@ impl AdminApiClient { runtime() .block_on(client.delete_domain_mask(&id, &domain_mask_id)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } fn create_jwt(&self, opts: RHash) -> Result<(), Error> { @@ -639,7 +646,7 @@ impl AdminApiClient { .map_err(map_err) } - fn create_request_filter(&self, opts: RHash) -> Result { + fn create_request_filter(&self, opts: RHash) -> Result { validate_keys(&opts, &["id", "methods"])?; let client = self.inner.clone(); let id = hash_require_string(&opts, "id")?; @@ -649,7 +656,7 @@ impl AdminApiClient { runtime() .block_on(client.create_request_filter(&id, ¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } fn update_request_filter(&self, opts: RHash) -> Result<(), Error> { @@ -693,7 +700,7 @@ impl AdminApiClient { .map_err(map_err) } - fn create_or_update_ip_custom_header(&self, opts: RHash) -> Result { + fn create_or_update_ip_custom_header(&self, opts: RHash) -> Result { validate_keys(&opts, &["id", "header_name"])?; let client = self.inner.clone(); let id = hash_require_string(&opts, "id")?; @@ -703,30 +710,30 @@ impl AdminApiClient { runtime() .block_on(client.create_or_update_ip_custom_header(&id, ¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn delete_ip_custom_header(&self, opts: RHash) -> Result { + fn delete_ip_custom_header(&self, opts: RHash) -> Result { validate_keys(&opts, &["id"])?; let client = self.inner.clone(); let id = hash_require_string(&opts, "id")?; runtime() .block_on(client.delete_ip_custom_header(&id)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn get_method_rate_limits(&self, opts: RHash) -> Result { + fn get_method_rate_limits(&self, opts: RHash) -> Result { validate_keys(&opts, &["id"])?; let client = self.inner.clone(); let id = hash_require_string(&opts, "id")?; runtime() .block_on(client.get_method_rate_limits(&id)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn create_method_rate_limit(&self, opts: RHash) -> Result { + fn create_method_rate_limit(&self, opts: RHash) -> Result { validate_keys(&opts, &["id", "interval", "methods", "rate"])?; let client = self.inner.clone(); let id = hash_require_string(&opts, "id")?; @@ -738,10 +745,10 @@ impl AdminApiClient { runtime() .block_on(client.create_method_rate_limit(&id, ¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn update_method_rate_limit(&self, opts: RHash) -> Result { + fn update_method_rate_limit(&self, opts: RHash) -> Result { validate_keys( &opts, &["id", "method_rate_limit_id", "methods", "status", "rate"], @@ -757,7 +764,7 @@ impl AdminApiClient { runtime() .block_on(client.update_method_rate_limit(&id, &method_rate_limit_id, ¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } fn delete_method_rate_limit(&self, opts: RHash) -> Result<(), Error> { @@ -786,7 +793,7 @@ impl AdminApiClient { .map_err(map_err) } - fn get_endpoint_metrics(&self, opts: RHash) -> Result { + fn get_endpoint_metrics(&self, opts: RHash) -> Result { validate_keys(&opts, &["id", "period", "metric"])?; let client = self.inner.clone(); let id = hash_require_string(&opts, "id")?; @@ -797,10 +804,10 @@ impl AdminApiClient { runtime() .block_on(client.get_endpoint_metrics(&id, ¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn get_account_metrics(&self, opts: RHash) -> Result { + fn get_account_metrics(&self, opts: RHash) -> Result { validate_keys(&opts, &["period", "metric", "percentile"])?; let client = self.inner.clone(); let params = core::admin::GetAccountMetricsRequest { @@ -811,42 +818,42 @@ impl AdminApiClient { runtime() .block_on(client.get_account_metrics(¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn list_chains(&self) -> Result { + fn list_chains(&self) -> Result { let client = self.inner.clone(); runtime() .block_on(client.list_chains()) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn list_invoices(&self) -> Result { + fn list_invoices(&self) -> Result { let client = self.inner.clone(); runtime() .block_on(client.list_invoices()) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn list_payments(&self) -> Result { + fn list_payments(&self) -> Result { let client = self.inner.clone(); runtime() .block_on(client.list_payments()) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn list_teams(&self) -> Result { + fn list_teams(&self) -> Result { let client = self.inner.clone(); runtime() .block_on(client.list_teams()) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn create_team(&self, opts: RHash) -> Result { + fn create_team(&self, opts: RHash) -> Result { validate_keys(&opts, &["name"])?; let client = self.inner.clone(); let params = core::admin::CreateTeamRequest { @@ -855,40 +862,40 @@ impl AdminApiClient { runtime() .block_on(client.create_team(¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn get_team(&self, opts: RHash) -> Result { + fn get_team(&self, opts: RHash) -> Result { validate_keys(&opts, &["id"])?; let client = self.inner.clone(); let id = hash_require_i64(&opts, "id")?; runtime() .block_on(client.get_team(id)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn delete_team(&self, opts: RHash) -> Result { + fn delete_team(&self, opts: RHash) -> Result { validate_keys(&opts, &["id"])?; let client = self.inner.clone(); let id = hash_require_i64(&opts, "id")?; runtime() .block_on(client.delete_team(id)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn list_team_endpoints(&self, opts: RHash) -> Result { + fn list_team_endpoints(&self, opts: RHash) -> Result { validate_keys(&opts, &["id"])?; let client = self.inner.clone(); let id = hash_require_i64(&opts, "id")?; runtime() .block_on(client.list_team_endpoints(id)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn update_team_endpoints(&self, opts: RHash) -> Result { + fn update_team_endpoints(&self, opts: RHash) -> Result { validate_keys(&opts, &["id", "endpoint_ids"])?; let client = self.inner.clone(); let id = hash_require_i64(&opts, "id")?; @@ -898,10 +905,10 @@ impl AdminApiClient { runtime() .block_on(client.update_team_endpoints(id, ¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn invite_team_member(&self, opts: RHash) -> Result { + fn invite_team_member(&self, opts: RHash) -> Result { validate_keys(&opts, &["id", "email", "full_name", "role"])?; let client = self.inner.clone(); let id = hash_require_i64(&opts, "id")?; @@ -913,10 +920,10 @@ impl AdminApiClient { runtime() .block_on(client.invite_team_member(id, ¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn remove_team_member(&self, opts: RHash) -> Result { + fn remove_team_member(&self, opts: RHash) -> Result { validate_keys(&opts, &["id", "user_id", "destroy_user"])?; let client = self.inner.clone(); let id = hash_require_i64(&opts, "id")?; @@ -927,10 +934,10 @@ impl AdminApiClient { runtime() .block_on(client.remove_team_member(id, user_id, ¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn resend_team_invite(&self, opts: RHash) -> Result { + fn resend_team_invite(&self, opts: RHash) -> Result { validate_keys(&opts, &["id", "user_id"])?; let client = self.inner.clone(); let id = hash_require_i64(&opts, "id")?; @@ -938,10 +945,10 @@ impl AdminApiClient { runtime() .block_on(client.resend_team_invite(id, user_id)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn bulk_update_endpoint_status(&self, opts: RHash) -> Result { + fn bulk_update_endpoint_status(&self, opts: RHash) -> Result { validate_keys(&opts, &["ids", "status"])?; let client = self.inner.clone(); let params = core::admin::BulkUpdateEndpointStatusRequest { @@ -951,10 +958,10 @@ impl AdminApiClient { runtime() .block_on(client.bulk_update_endpoint_status(¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn bulk_add_tag(&self, opts: RHash) -> Result { + fn bulk_add_tag(&self, opts: RHash) -> Result { validate_keys(&opts, &["ids", "label"])?; let client = self.inner.clone(); let params = core::admin::BulkAddTagRequest { @@ -964,10 +971,10 @@ impl AdminApiClient { runtime() .block_on(client.bulk_add_tag(¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn bulk_remove_tag(&self, opts: RHash) -> Result { + fn bulk_remove_tag(&self, opts: RHash) -> Result { validate_keys(&opts, &["ids", "tag_id"])?; let client = self.inner.clone(); let params = core::admin::BulkRemoveTagRequest { @@ -977,18 +984,18 @@ impl AdminApiClient { runtime() .block_on(client.bulk_remove_tag(¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn list_tags(&self) -> Result { + fn list_tags(&self) -> Result { let client = self.inner.clone(); runtime() .block_on(client.list_tags()) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn rename_tag(&self, opts: RHash) -> Result { + fn rename_tag(&self, opts: RHash) -> Result { validate_keys(&opts, &["id", "label"])?; let client = self.inner.clone(); let id = hash_require_i32(&opts, "id")?; @@ -998,20 +1005,20 @@ impl AdminApiClient { runtime() .block_on(client.rename_tag(id, ¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn delete_account_tag(&self, opts: RHash) -> Result { + fn delete_account_tag(&self, opts: RHash) -> Result { validate_keys(&opts, &["id"])?; let client = self.inner.clone(); let id = hash_require_i32(&opts, "id")?; runtime() .block_on(client.delete_account_tag(id)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn get_usage_by_tag(&self, opts: RHash) -> Result { + fn get_usage_by_tag(&self, opts: RHash) -> Result { validate_keys(&opts, &["start_time", "end_time"])?; let client = self.inner.clone(); let params = core::admin::GetUsageRequest { @@ -1021,17 +1028,17 @@ impl AdminApiClient { runtime() .block_on(client.get_usage_by_tag(¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn get_endpoint_security(&self, opts: RHash) -> Result { + fn get_endpoint_security(&self, opts: RHash) -> Result { validate_keys(&opts, &["id"])?; let client = self.inner.clone(); let id = hash_require_string(&opts, "id")?; runtime() .block_on(client.get_endpoint_security(&id)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } } @@ -1227,7 +1234,7 @@ impl DestinationAttributes { // ── StreamsApiClient ──────────────────────────────────────────────────────── -#[magnus::wrap(class = "QuicknodeSdk::Streams", free_immediately, size)] +#[magnus::wrap(class = "QuicknodeSdk::Native::Streams", free_immediately, size)] #[derive(Clone)] pub struct StreamsApiClient { inner: core::streams::StreamsApiClient, @@ -1238,7 +1245,7 @@ impl StreamsApiClient { // create_stream accepts a Ruby Hash because the param count exceeds magnus arity limit of 15. // Required keys: name, network, dataset, region, start_range, end_range, // destination_attributes, plan, threshold_fetch_buffer - fn create_stream(&self, opts: RHash) -> Result { + fn create_stream(&self, opts: RHash) -> Result { let client = self.inner.clone(); let name = hash_require_string(&opts, "name")?; let network = hash_require_string(&opts, "network")?; @@ -1298,10 +1305,10 @@ impl StreamsApiClient { runtime() .block_on(client.create_stream(¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn list_streams(&self, opts: RHash) -> Result { + fn list_streams(&self, opts: RHash) -> Result { validate_keys( &opts, &[ @@ -1323,7 +1330,7 @@ impl StreamsApiClient { runtime() .block_on(client.list_streams(¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } fn delete_all_streams(&self) -> Result<(), Error> { @@ -1333,18 +1340,18 @@ impl StreamsApiClient { .map_err(map_err) } - fn get_stream(&self, opts: RHash) -> Result { + fn get_stream(&self, opts: RHash) -> Result { validate_keys(&opts, &["id"])?; let client = self.inner.clone(); let id = hash_require_string(&opts, "id")?; runtime() .block_on(client.get_stream(&id)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } // update_stream accepts id + a Ruby Hash (opts) because the param count exceeds 15. - fn update_stream(&self, opts: RHash) -> Result { + fn update_stream(&self, opts: RHash) -> Result { let client = self.inner.clone(); let id = hash_require_string(&opts, "id")?; let dataset = @@ -1391,7 +1398,7 @@ impl StreamsApiClient { runtime() .block_on(client.update_stream(&id, ¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } fn delete_stream(&self, opts: RHash) -> Result<(), Error> { @@ -1421,7 +1428,7 @@ impl StreamsApiClient { .map_err(map_err) } - fn test_filter(&self, opts: RHash) -> Result { + fn test_filter(&self, opts: RHash) -> Result { validate_keys( &opts, &[ @@ -1450,23 +1457,23 @@ impl StreamsApiClient { runtime() .block_on(client.test_filter(¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn get_enabled_count(&self, opts: RHash) -> Result { + fn get_enabled_count(&self, opts: RHash) -> Result { validate_keys(&opts, &["stream_type"])?; let client = self.inner.clone(); let stream_type = hash_get_string(&opts, "stream_type")?; runtime() .block_on(client.get_enabled_count(stream_type.as_deref())) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } } // ── WebhooksApiClient ─────────────────────────────────────────────────────── -#[magnus::wrap(class = "QuicknodeSdk::Webhooks", free_immediately, size)] +#[magnus::wrap(class = "QuicknodeSdk::Native::Webhooks", free_immediately, size)] #[derive(Clone)] pub struct WebhooksApiClient { inner: core::webhooks::WebhooksApiClient, @@ -1474,7 +1481,7 @@ pub struct WebhooksApiClient { #[allow(clippy::needless_pass_by_value)] impl WebhooksApiClient { - fn list_webhooks(&self, opts: RHash) -> Result { + fn list_webhooks(&self, opts: RHash) -> Result { validate_keys(&opts, &["limit", "offset"])?; let client = self.inner.clone(); let params = core::webhooks::GetWebhooksParams { @@ -1484,7 +1491,7 @@ impl WebhooksApiClient { runtime() .block_on(client.list_webhooks(¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } fn delete_all_webhooks(&self) -> Result<(), Error> { @@ -1494,17 +1501,17 @@ impl WebhooksApiClient { .map_err(map_err) } - fn get_webhook(&self, opts: RHash) -> Result { + fn get_webhook(&self, opts: RHash) -> Result { validate_keys(&opts, &["id"])?; let client = self.inner.clone(); let id = hash_require_string(&opts, "id")?; runtime() .block_on(client.get_webhook(&id)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn update_webhook(&self, opts: RHash) -> Result { + fn update_webhook(&self, opts: RHash) -> Result { validate_keys( &opts, &[ @@ -1531,7 +1538,7 @@ impl WebhooksApiClient { runtime() .block_on(client.update_webhook(&id, ¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } fn delete_webhook(&self, opts: RHash) -> Result<(), Error> { @@ -1566,15 +1573,15 @@ impl WebhooksApiClient { .map_err(map_err) } - fn get_enabled_count(&self) -> Result { + fn get_enabled_count(&self) -> Result { let client = self.inner.clone(); runtime() .block_on(client.get_enabled_count()) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn create_webhook_from_template(&self, opts: RHash) -> Result { + fn create_webhook_from_template(&self, opts: RHash) -> Result { validate_keys( &opts, &[ @@ -1603,10 +1610,10 @@ impl WebhooksApiClient { runtime() .block_on(client.create_webhook_from_template(¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn update_webhook_template(&self, opts: RHash) -> Result { + fn update_webhook_template(&self, opts: RHash) -> Result { validate_keys( &opts, &[ @@ -1630,13 +1637,13 @@ impl WebhooksApiClient { runtime() .block_on(client.update_webhook_template(&webhook_id, ¶ms)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } } // ── KvStoreApiClient ──────────────────────────────────────────────────────── -#[magnus::wrap(class = "QuicknodeSdk::KvStore", free_immediately, size)] +#[magnus::wrap(class = "QuicknodeSdk::Native::KvStore", free_immediately, size)] #[derive(Clone)] pub struct KvStoreApiClient { inner: core::kvstore::KvStoreApiClient, @@ -1655,7 +1662,7 @@ impl KvStoreApiClient { .map_err(map_err) } - fn get_sets(&self, opts: RHash) -> Result { + fn get_sets(&self, opts: RHash) -> Result { validate_keys(&opts, &["limit", "cursor"])?; let client = self.inner.clone(); runtime() @@ -1664,17 +1671,17 @@ impl KvStoreApiClient { cursor: hash_get_string(&opts, "cursor")?, })) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn get_set(&self, opts: RHash) -> Result { + fn get_set(&self, opts: RHash) -> Result { validate_keys(&opts, &["key"])?; let client = self.inner.clone(); let key = hash_require_string(&opts, "key")?; runtime() .block_on(client.get_set(&key)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } fn bulk_sets(&self, opts: RHash) -> Result<(), Error> { @@ -1706,7 +1713,7 @@ impl KvStoreApiClient { .map_err(map_err) } - fn get_lists(&self, opts: RHash) -> Result { + fn get_lists(&self, opts: RHash) -> Result { validate_keys(&opts, &["limit", "cursor"])?; let client = self.inner.clone(); runtime() @@ -1715,10 +1722,10 @@ impl KvStoreApiClient { cursor: hash_get_string(&opts, "cursor")?, })) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } - fn get_list(&self, opts: RHash) -> Result { + fn get_list(&self, opts: RHash) -> Result { validate_keys(&opts, &["key", "limit", "cursor"])?; let client = self.inner.clone(); let key = hash_require_string(&opts, "key")?; @@ -1731,7 +1738,7 @@ impl KvStoreApiClient { }, )) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } fn update_list(&self, opts: RHash) -> Result<(), Error> { @@ -1763,7 +1770,7 @@ impl KvStoreApiClient { .map_err(map_err) } - fn list_contains_item(&self, opts: RHash) -> Result { + fn list_contains_item(&self, opts: RHash) -> Result { validate_keys(&opts, &["key", "item"])?; let client = self.inner.clone(); let key = hash_require_string(&opts, "key")?; @@ -1771,7 +1778,7 @@ impl KvStoreApiClient { runtime() .block_on(client.list_contains_item(&key, &item)) .map_err(map_err) - .and_then(to_json) + .and_then(to_ruby) } fn delete_list_item(&self, opts: RHash) -> Result<(), Error> { @@ -1804,8 +1811,10 @@ fn init(ruby: &Ruby) -> Result<(), Error> { // can use them from the first call onward. errors::init(ruby, &module)?; + let native = module.define_module("Native")?; + // ── SDK root ────────────────────────────────────────────── - let sdk = module.define_class("SDK", ruby.class_object())?; + let sdk = native.define_class("SDK", ruby.class_object())?; sdk.define_singleton_method("from_env", function!(QuicknodeSdk::from_env, 0))?; sdk.define_method("admin", method!(QuicknodeSdk::admin, 0))?; sdk.define_method("streams", method!(QuicknodeSdk::streams, 0))?; @@ -1813,7 +1822,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> { sdk.define_method("kvstore", method!(QuicknodeSdk::kvstore, 0))?; // ── Admin ───────────────────────────────────────────────── - let admin = module.define_class("Admin", ruby.class_object())?; + let admin = native.define_class("Admin", ruby.class_object())?; admin.define_method("get_endpoints", method!(AdminApiClient::get_endpoints, 1))?; admin.define_method( "create_endpoint", @@ -2011,7 +2020,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> { dest_attrs.define_singleton_method("redis", function!(DestinationAttributes::redis, 1))?; // ── Streams ─────────────────────────────────────────────── - let streams = module.define_class("Streams", ruby.class_object())?; + let streams = native.define_class("Streams", ruby.class_object())?; streams.define_method("create_stream", method!(StreamsApiClient::create_stream, 1))?; streams.define_method("list_streams", method!(StreamsApiClient::list_streams, 1))?; streams.define_method( @@ -2033,7 +2042,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> { )?; // ── Webhooks ────────────────────────────────────────────── - let webhooks = module.define_class("Webhooks", ruby.class_object())?; + let webhooks = native.define_class("Webhooks", ruby.class_object())?; webhooks.define_method( "list_webhooks", method!(WebhooksApiClient::list_webhooks, 1), @@ -2073,7 +2082,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> { )?; // ── KvStore ─────────────────────────────────────────────── - let kvstore = module.define_class("KvStore", ruby.class_object())?; + let kvstore = native.define_class("KvStore", ruby.class_object())?; kvstore.define_method("create_set", method!(KvStoreApiClient::create_set, 1))?; kvstore.define_method("get_sets", method!(KvStoreApiClient::get_sets, 1))?; kvstore.define_method("get_set", method!(KvStoreApiClient::get_set, 1))?; diff --git a/ruby/.solargraph.yml b/ruby/.solargraph.yml new file mode 100644 index 0000000..690a5d0 --- /dev/null +++ b/ruby/.solargraph.yml @@ -0,0 +1,8 @@ +include: + - "lib/**/*.rb" +exclude: + - "lib/quicknode_sdk.bundle" + - "examples/**/*" +require_paths: + - lib + - sig diff --git a/ruby/Steepfile b/ruby/Steepfile new file mode 100644 index 0000000..0fb9485 --- /dev/null +++ b/ruby/Steepfile @@ -0,0 +1,3 @@ +target :lib do + signature "sig" +end diff --git a/ruby/examples/admin.rb b/ruby/examples/admin.rb index 3889574..de94878 100644 --- a/ruby/examples/admin.rb +++ b/ruby/examples/admin.rb @@ -1,29 +1,28 @@ -require "json" require_relative "../lib/quicknode_sdk" qn = QuicknodeSdk::SDK.from_env -response = JSON.parse(qn.admin.get_endpoints( +response = qn.admin.get_endpoints( limit: 20, sort_by: "created_at", sort_direction: "desc" -)) +) -if response["pagination"] - p = response["pagination"] - puts "#{response["data"].length} of #{p["total"]} (offset #{p["offset"]}, limit #{p["limit"]})" +if response[:pagination] + p = response[:pagination] + puts "#{response[:data].length} of #{p[:total]} (offset #{p[:offset]}, limit #{p[:limit]})" end -response["data"].each do |ep| - puts "#{ep["id"]} | #{ep["name"]} | #{ep["status"]} | #{ep["network"]} | " \ - "dedicated=#{ep["is_dedicated"]} flat=#{ep["is_flat_rate"]}" +response[:data].each do |ep| + puts "#{ep[:id]} | #{ep[:name]} | #{ep[:status]} | #{ep[:network]} | " \ + "dedicated=#{ep[:is_dedicated]} flat=#{ep[:is_flat_rate]}" end -tags = JSON.parse(qn.admin.list_tags) -puts "account tags: #{tags.dig("data", "tags")&.length || 0}" +tags = qn.admin.list_tags +puts "account tags: #{tags.dig(:data, :tags)&.length || 0}" -first = response["data"].first +first = response[:data].first if first - sec = JSON.parse(qn.admin.get_endpoint_security(id: first["id"])) - puts "get_endpoint_security: has_data=#{!sec["data"].nil?}" + sec = qn.admin.get_endpoint_security(id: first[:id]) + puts "get_endpoint_security: has_data=#{!sec[:data].nil?}" end diff --git a/ruby/examples/admin_e2e.rb b/ruby/examples/admin_e2e.rb index 66e33c4..2fb2cf3 100644 --- a/ruby/examples/admin_e2e.rb +++ b/ruby/examples/admin_e2e.rb @@ -1,4 +1,3 @@ -require "json" require "securerandom" require_relative "../lib/quicknode_sdk" @@ -6,73 +5,95 @@ # ── Read-only globals ───────────────────────────────────────────────────────── -resp = JSON.parse(qn.admin.list_chains) -puts "list_chains: #{resp["data"].length} chains" - -resp = JSON.parse(qn.admin.get_endpoints(limit: 5)) -puts "get_endpoints: #{resp["data"].length} endpoints" - -resp = JSON.parse(qn.admin.get_usage({})) -puts "get_usage: #{resp["data"].inspect}" +# Exercise all three call styles the dispatcher supports against an arity-0 +# native method (list_chains): bare, positional empty hash, and kwargs splat. +resp = qn.admin.list_chains +puts "list_chains: #{resp[:data].length} chains" +raise "list_chains positional hash broke" unless qn.admin.list_chains({})[:data].length == resp[:data].length +raise "list_chains kwargs splat broke" unless qn.admin.list_chains(**{})[:data].length == resp[:data].length + +# Same coverage for an arity-1 native method (get_endpoints) — this is the +# call shape the README advertises bare, so all three must work. +bare = qn.admin.get_endpoints +kwargs = qn.admin.get_endpoints(limit: 5) +positional = qn.admin.get_endpoints({}) +puts "get_endpoints: #{kwargs[:data].length} endpoints (bare=#{bare[:data].length}, positional=#{positional[:data].length})" +raise "get_endpoints bare vs positional differ" unless bare.keys.sort == positional.keys.sort + +# Verify symbol- and string-key access work interchangeably. +sample = qn.admin.get_endpoints(limit: 1) +raise "expected QuicknodeSdk::IndifferentHash" unless sample.is_a?(QuicknodeSdk::IndifferentHash) +raise "indifferent access broken" unless sample[:data] == sample["data"] +puts "indifferent access: ok" + +# Same three-call-style check for one of the arity-1 read-only usage methods. +# The remaining get_usage_by_* methods share the dispatcher path, so one +# representative is enough to lock in coverage. +usage_bare = qn.admin.get_usage +usage_positional = qn.admin.get_usage({}) +usage_kwargs = qn.admin.get_usage(**{}) +raise "get_usage call styles diverge" unless usage_bare.keys.sort == usage_positional.keys.sort && usage_bare.keys.sort == usage_kwargs.keys.sort +puts "get_usage: #{usage_bare[:data].inspect}" +resp = usage_bare sleep 0.5 -resp = JSON.parse(qn.admin.get_usage_by_endpoint({})) -puts "get_usage_by_endpoint: #{resp["data"].inspect}" +resp = qn.admin.get_usage_by_endpoint({}) +puts "get_usage_by_endpoint: #{resp[:data].inspect}" -resp = JSON.parse(qn.admin.get_usage_by_method({})) -puts "get_usage_by_method: #{resp["data"].inspect}" +resp = qn.admin.get_usage_by_method({}) +puts "get_usage_by_method: #{resp[:data].inspect}" sleep 0.5 -resp = JSON.parse(qn.admin.get_usage_by_chain({})) -puts "get_usage_by_chain: #{resp["data"].inspect}" +resp = qn.admin.get_usage_by_chain({}) +puts "get_usage_by_chain: #{resp[:data].inspect}" -resp = JSON.parse(qn.admin.get_usage_by_tag({})) -puts "get_usage_by_tag: #{resp["data"].inspect}" +resp = qn.admin.get_usage_by_tag({}) +puts "get_usage_by_tag: #{resp[:data].inspect}" -resp = JSON.parse(qn.admin.list_tags) -puts "list_tags: #{resp.dig("data", "tags")&.length || 0} tags" +resp = qn.admin.list_tags +puts "list_tags: #{resp.dig(:data, :tags)&.length || 0} tags" -resp = JSON.parse(qn.admin.get_account_metrics(period: "day", metric: "requests")) -puts "get_account_metrics: #{resp["data"].length} series" +resp = qn.admin.get_account_metrics(period: "day", metric: "requests") +puts "get_account_metrics: #{resp[:data].length} series" -resp = JSON.parse(qn.admin.list_invoices) -puts "list_invoices: #{resp["data"].inspect}" +resp = qn.admin.list_invoices +puts "list_invoices: #{resp[:data].inspect}" -resp = JSON.parse(qn.admin.list_payments) -puts "list_payments: #{resp["data"].inspect}" +resp = qn.admin.list_payments +puts "list_payments: #{resp[:data].inspect}" -resp = JSON.parse(qn.admin.list_teams) -puts "list_teams: #{resp["data"].length} teams" +resp = qn.admin.list_teams +puts "list_teams: #{resp[:data].length} teams" sleep 0.5 # ── Create endpoint ─────────────────────────────────────────────────────────── -resp = JSON.parse(qn.admin.create_endpoint(chain: "ethereum", network: "mainnet")) -endpoint_id = resp.dig("data", "id") +resp = qn.admin.create_endpoint(chain: "ethereum", network: "mainnet") +endpoint_id = resp.dig(:data, :id) unless endpoint_id - puts "create_endpoint failed: #{resp["error"]}" + puts "create_endpoint failed: #{resp[:error]}" exit 1 end -puts "create_endpoint: #{endpoint_id} (#{resp.dig("data", "http_url")})" +puts "create_endpoint: #{endpoint_id} (#{resp.dig(:data, :http_url)})" # ── Endpoint CRUD ───────────────────────────────────────────────────────────── -resp = JSON.parse(qn.admin.show_endpoint(id: endpoint_id)) -puts "show_endpoint: #{resp.dig("data", "id")}" +resp = qn.admin.show_endpoint(id: endpoint_id) +puts "show_endpoint: #{resp.dig(:data, :id)}" qn.admin.update_endpoint(id: endpoint_id, label: "sdk-example") puts "update_endpoint: ok" sleep 0.5 -resp = JSON.parse(qn.admin.update_endpoint_status(id: endpoint_id, status: "paused")) -puts "update_endpoint_status paused: #{resp["data"]}" +resp = qn.admin.update_endpoint_status(id: endpoint_id, status: "paused") +puts "update_endpoint_status paused: #{resp[:data]}" -resp = JSON.parse(qn.admin.update_endpoint_status(id: endpoint_id, status: "active")) -puts "update_endpoint_status active: #{resp["data"]}" +resp = qn.admin.update_endpoint_status(id: endpoint_id, status: "active") +puts "update_endpoint_status active: #{resp[:data]}" sleep 1 @@ -82,8 +103,8 @@ puts "create_tag: ok" sleep 0.5 -resp = JSON.parse(qn.admin.show_endpoint(id: endpoint_id)) -tag_id = resp.dig("data", "tags", 0, "tag_id")&.to_s +resp = qn.admin.show_endpoint(id: endpoint_id) +tag_id = resp.dig(:data, :tags, 0, :tag_id)&.to_s if tag_id qn.admin.delete_tag(id: endpoint_id, tag_id: tag_id) puts "delete_tag: ok" @@ -91,30 +112,30 @@ # ── Logs & metrics ──────────────────────────────────────────────────────────── -resp = JSON.parse(qn.admin.get_endpoint_logs( +resp = qn.admin.get_endpoint_logs( id: endpoint_id, from_time: "2025-01-01T00:00:00Z", to_time: "2025-01-02T00:00:00Z" -)) -puts "get_endpoint_logs: #{resp["data"].length} entries" +) +puts "get_endpoint_logs: #{resp[:data].length} entries" sleep 1 -resp = JSON.parse(qn.admin.get_endpoint_metrics(id: endpoint_id, period: "day", metric: "credits_over_time")) -puts "get_endpoint_metrics: #{resp["data"].length} series" +resp = qn.admin.get_endpoint_metrics(id: endpoint_id, period: "day", metric: "credits_over_time") +puts "get_endpoint_metrics: #{resp[:data].length} series" # ── Security options ────────────────────────────────────────────────────────── -resp = JSON.parse(qn.admin.get_security_options(id: endpoint_id)) -puts "get_security_options: #{resp["data"].length} options" +resp = qn.admin.get_security_options(id: endpoint_id) +puts "get_security_options: #{resp[:data].length} options" sleep 1 -resp = JSON.parse(qn.admin.get_endpoint_security(id: endpoint_id)) -puts "get_endpoint_security: has_data=#{!resp["data"].nil?}" +resp = qn.admin.get_endpoint_security(id: endpoint_id) +puts "get_endpoint_security: has_data=#{!resp[:data].nil?}" -resp = JSON.parse(qn.admin.update_security_options(id: endpoint_id, tokens: "enabled")) -puts "update_security_options: #{resp["data"].length} options" +resp = qn.admin.update_security_options(id: endpoint_id, tokens: "enabled") +puts "update_security_options: #{resp[:data].length} options" sleep 0.5 @@ -124,11 +145,11 @@ qn.admin.create_token(id: endpoint_id) puts "create_token: ok" -resp = JSON.parse(qn.admin.show_endpoint(id: endpoint_id)) -token_id = resp.dig("data", "security", "tokens", 0, "id") +resp = qn.admin.show_endpoint(id: endpoint_id) +token_id = resp.dig(:data, :security, :tokens, 0, :id) if token_id - resp = JSON.parse(qn.admin.delete_token(id: endpoint_id, token_id: token_id)) - puts "delete_token: #{resp["data"]}" + resp = qn.admin.delete_token(id: endpoint_id, token_id: token_id) + puts "delete_token: #{resp[:data]}" end # ── Referrer ────────────────────────────────────────────────────────────────── @@ -137,11 +158,11 @@ puts "create_referrer: ok" sleep 0.5 -resp = JSON.parse(qn.admin.show_endpoint(id: endpoint_id)) -referrer_id = resp.dig("data", "security", "referrers", 0, "id") +resp = qn.admin.show_endpoint(id: endpoint_id) +referrer_id = resp.dig(:data, :security, :referrers, 0, :id) if referrer_id - resp = JSON.parse(qn.admin.delete_referrer(id: endpoint_id, referrer_id: referrer_id)) - puts "delete_referrer: #{resp["data"]}" + resp = qn.admin.delete_referrer(id: endpoint_id, referrer_id: referrer_id) + puts "delete_referrer: #{resp[:data]}" end # ── IP allowlist ────────────────────────────────────────────────────────────── @@ -149,13 +170,13 @@ qn.admin.create_ip(id: endpoint_id, ip: "192.0.2.1") puts "create_ip: ok" -resp = JSON.parse(qn.admin.show_endpoint(id: endpoint_id)) -ip_id = resp.dig("data", "security", "ips", 0, "id") +resp = qn.admin.show_endpoint(id: endpoint_id) +ip_id = resp.dig(:data, :security, :ips, 0, :id) sleep 0.5 if ip_id - resp = JSON.parse(qn.admin.delete_ip(id: endpoint_id, ip_id: ip_id)) - puts "delete_ip: #{resp["data"]}" + resp = qn.admin.delete_ip(id: endpoint_id, ip_id: ip_id) + puts "delete_ip: #{resp[:data]}" end # ── Domain mask ─────────────────────────────────────────────────────────────── @@ -163,11 +184,11 @@ qn.admin.create_domain_mask(id: endpoint_id, domain_mask: "example.com") puts "create_domain_mask: ok" -resp = JSON.parse(qn.admin.show_endpoint(id: endpoint_id)) -mask_id = resp.dig("data", "security", "domain_masks", 0, "id") +resp = qn.admin.show_endpoint(id: endpoint_id) +mask_id = resp.dig(:data, :security, :domain_masks, 0, :id) if mask_id - resp = JSON.parse(qn.admin.delete_domain_mask(id: endpoint_id, domain_mask_id: mask_id)) - puts "delete_domain_mask: #{resp["data"]}" + resp = qn.admin.delete_domain_mask(id: endpoint_id, domain_mask_id: mask_id) + puts "delete_domain_mask: #{resp[:data]}" end # ── JWT (placeholder key will fail) ────────────────────────────────────────── @@ -184,8 +205,8 @@ puts "create_jwt error (expected with placeholder key): #{e}" end -resp = JSON.parse(qn.admin.show_endpoint(id: endpoint_id)) -jwt_id = resp.dig("data", "security", "jwts", 0, "id") +resp = qn.admin.show_endpoint(id: endpoint_id) +jwt_id = resp.dig(:data, :security, :jwts, 0, :id) if jwt_id qn.admin.delete_jwt(id: endpoint_id, jwt_id: jwt_id) puts "delete_jwt: ok" @@ -193,9 +214,9 @@ # ── Request filter ──────────────────────────────────────────────────────────── -resp = JSON.parse(qn.admin.create_request_filter(id: endpoint_id, methods: ["eth_getBalance"])) -rf_id = resp.dig("data", "id") -puts "create_request_filter: #{resp["data"]}" +resp = qn.admin.create_request_filter(id: endpoint_id, methods: ["eth_getBalance"]) +rf_id = resp.dig(:data, :id) +puts "create_request_filter: #{resp[:data]}" if rf_id qn.admin.update_request_filter(id: endpoint_id, request_filter_id: rf_id, methods: ["eth_call"]) @@ -207,11 +228,11 @@ # ── IP custom header ────────────────────────────────────────────────────────── -resp = JSON.parse(qn.admin.create_or_update_ip_custom_header(id: endpoint_id, header_name: "X-Custom-Header")) -puts "create_or_update_ip_custom_header: #{resp["data"]}" +resp = qn.admin.create_or_update_ip_custom_header(id: endpoint_id, header_name: "X-Custom-Header") +puts "create_or_update_ip_custom_header: #{resp[:data]}" -resp = JSON.parse(qn.admin.delete_ip_custom_header(id: endpoint_id)) -puts "delete_ip_custom_header: #{resp["data"]}" +resp = qn.admin.delete_ip_custom_header(id: endpoint_id) +puts "delete_ip_custom_header: #{resp[:data]}" sleep 0.5 @@ -220,16 +241,16 @@ #qn.admin.update_rate_limits(id: endpoint_id, rps: 3) #puts "update_rate_limits: ok" -resp = JSON.parse(qn.admin.get_method_rate_limits(id: endpoint_id)) -puts "get_method_rate_limits: #{resp["data"]}" +resp = qn.admin.get_method_rate_limits(id: endpoint_id) +puts "get_method_rate_limits: #{resp[:data]}" -resp = JSON.parse(qn.admin.create_method_rate_limit(id: endpoint_id, interval: "second", methods: ["eth_call"], rate: 5)) -mrl_id = resp.dig("data", "id") -puts "create_method_rate_limit: #{resp["data"]}" +resp = qn.admin.create_method_rate_limit(id: endpoint_id, interval: "second", methods: ["eth_call"], rate: 5) +mrl_id = resp.dig(:data, :id) +puts "create_method_rate_limit: #{resp[:data]}" if mrl_id - resp = JSON.parse(qn.admin.update_method_rate_limit(id: endpoint_id, method_rate_limit_id: mrl_id, rate: 10)) - puts "update_method_rate_limit: #{resp["data"]}" + resp = qn.admin.update_method_rate_limit(id: endpoint_id, method_rate_limit_id: mrl_id, rate: 10) + puts "update_method_rate_limit: #{resp[:data]}" qn.admin.delete_method_rate_limit(id: endpoint_id, method_rate_limit_id: mrl_id) puts "delete_method_rate_limit: ok" @@ -249,66 +270,66 @@ # ── Bulk endpoint ops (single-endpoint batch) ──────────────────────────────── -resp = JSON.parse(qn.admin.bulk_update_endpoint_status(ids: [endpoint_id], status: "paused")) -puts "bulk_update_endpoint_status paused: #{resp["data"]}" +resp = qn.admin.bulk_update_endpoint_status(ids: [endpoint_id], status: "paused") +puts "bulk_update_endpoint_status paused: #{resp[:data]}" -resp = JSON.parse(qn.admin.bulk_update_endpoint_status(ids: [endpoint_id], status: "active")) -puts "bulk_update_endpoint_status active: #{resp["data"]}" +resp = qn.admin.bulk_update_endpoint_status(ids: [endpoint_id], status: "active") +puts "bulk_update_endpoint_status active: #{resp[:data]}" # ── Account-level tags (bulk_add/remove + rename/delete) ───────────────────── tag_suffix = SecureRandom.hex(4) -resp = JSON.parse(qn.admin.bulk_add_tag(ids: [endpoint_id], label: "sdk-bulk-#{tag_suffix}")) -puts "bulk_add_tag: #{resp["data"]}" -bulk_tag_id = resp.dig("data", "tag", "tag_id") +resp = qn.admin.bulk_add_tag(ids: [endpoint_id], label: "sdk-bulk-#{tag_suffix}") +puts "bulk_add_tag: #{resp[:data]}" +bulk_tag_id = resp.dig(:data, :tag, :tag_id) sleep 0.5 if bulk_tag_id - resp = JSON.parse(qn.admin.rename_tag(id: bulk_tag_id, label: "sdk-renamed-#{tag_suffix}")) - puts "rename_tag: #{resp["data"]}" + resp = qn.admin.rename_tag(id: bulk_tag_id, label: "sdk-renamed-#{tag_suffix}") + puts "rename_tag: #{resp[:data]}" - resp = JSON.parse(qn.admin.bulk_remove_tag(ids: [endpoint_id], tag_id: bulk_tag_id)) - puts "bulk_remove_tag: #{resp["data"]}" + resp = qn.admin.bulk_remove_tag(ids: [endpoint_id], tag_id: bulk_tag_id) + puts "bulk_remove_tag: #{resp[:data]}" - resp = JSON.parse(qn.admin.delete_account_tag(id: bulk_tag_id)) - puts "delete_account_tag: #{resp["data"]}" + resp = qn.admin.delete_account_tag(id: bulk_tag_id) + puts "delete_account_tag: #{resp[:data]}" end sleep 0.5 # ── Teams ───────────────────────────────────────────────────────────────────── -resp = JSON.parse(qn.admin.create_team(name: "sdk-example-team")) -team_id = resp.dig("data", "id") -puts "create_team: #{resp["data"]}" +resp = qn.admin.create_team(name: "sdk-example-team") +team_id = resp.dig(:data, :id) +puts "create_team: #{resp[:data]}" sleep 0.5 if team_id - resp = JSON.parse(qn.admin.get_team(id: team_id)) - puts "get_team: #{resp.dig("data", "name")}" + resp = qn.admin.get_team(id: team_id) + puts "get_team: #{resp.dig(:data, :name)}" - resp = JSON.parse(qn.admin.list_team_endpoints(id: team_id)) - puts "list_team_endpoints: #{resp["data"].length} endpoints" + resp = qn.admin.list_team_endpoints(id: team_id) + puts "list_team_endpoints: #{resp[:data].length} endpoints" sleep 0.5 - resp = JSON.parse(qn.admin.update_team_endpoints(id: team_id, endpoint_ids: [endpoint_id])) - puts "update_team_endpoints: #{resp["data"]}" + resp = qn.admin.update_team_endpoints(id: team_id, endpoint_ids: [endpoint_id]) + puts "update_team_endpoints: #{resp[:data]}" sleep 0.5 begin - resp = JSON.parse(qn.admin.invite_team_member(id: team_id, email: "placeholder@example.com")) - puts "invite_team_member: #{resp["data"]}" + resp = qn.admin.invite_team_member(id: team_id, email: "placeholder@example.com") + puts "invite_team_member: #{resp[:data]}" rescue => e puts "invite_team_member error (expected with placeholder email): #{e}" end sleep 0.5 - resp = JSON.parse(qn.admin.delete_team(id: team_id)) - puts "delete_team: #{resp["data"]}" + resp = qn.admin.delete_team(id: team_id) + puts "delete_team: #{resp[:data]}" end sleep 0.5 diff --git a/ruby/examples/kvstore_e2e.rb b/ruby/examples/kvstore_e2e.rb index 8a179f0..f66c491 100644 --- a/ruby/examples/kvstore_e2e.rb +++ b/ruby/examples/kvstore_e2e.rb @@ -1,4 +1,3 @@ -require "json" require_relative "../lib/quicknode_sdk" qn = QuicknodeSdk::SDK.from_env @@ -8,11 +7,14 @@ qn.kvstore.create_set(key: "e2e-set-key", value: "e2e-value") puts "created set: e2e-set-key" -set = JSON.parse(qn.kvstore.get_set(key: "e2e-set-key")) -puts "get set: #{set["value"]}" +set = qn.kvstore.get_set(key: "e2e-set-key") +puts "get set: #{set[:value]}" -sets = JSON.parse(qn.kvstore.get_sets({})) -puts "all sets: #{sets["data"].map { |e| e["key"] }.inspect}" +# get_sets is arity-1 native: exercise bare, kwargs, and positional hash. +sets = qn.kvstore.get_sets({}) +puts "all sets: #{sets[:data].map { |e| e[:key] }.inspect}" +raise "get_sets bare broke" unless qn.kvstore.get_sets.keys.sort == sets.keys.sort +raise "get_sets kwargs splat broke" unless qn.kvstore.get_sets(**{}).keys.sort == sets.keys.sort qn.kvstore.bulk_sets(delete_sets: ["e2e-set-key"]) puts "bulk sets: deleted e2e-set-key" @@ -28,17 +30,20 @@ qn.kvstore.create_list(key: "e2e-list-key", items: ["0xabc", "0xdef"]) puts "created list: e2e-list-key" -list = JSON.parse(qn.kvstore.get_list(key: "e2e-list-key")) -puts "get list items: #{list["data"]["items"].inspect}" +list = qn.kvstore.get_list(key: "e2e-list-key") +puts "get list items: #{list.dig(:data, :items).inspect}" -lists = JSON.parse(qn.kvstore.get_lists({})) -puts "all list keys: #{lists["data"]["keys"].inspect}" +# get_lists is arity-1 native: exercise bare, kwargs, and positional hash. +lists = qn.kvstore.get_lists({}) +puts "all list keys: #{lists.dig(:data, :keys).inspect}" +raise "get_lists bare broke" unless qn.kvstore.get_lists.keys.sort == lists.keys.sort +raise "get_lists kwargs splat broke" unless qn.kvstore.get_lists(**{}).keys.sort == lists.keys.sort qn.kvstore.add_list_item(key: "e2e-list-key", item: "0x123") puts "added list item: 0x123" -contains = JSON.parse(qn.kvstore.list_contains_item(key: "e2e-list-key", item: "0x123")) -puts "list contains 0x123: #{contains["exists"]}" +contains = qn.kvstore.list_contains_item(key: "e2e-list-key", item: "0x123") +puts "list contains 0x123: #{contains[:exists]}" qn.kvstore.update_list(key: "e2e-list-key", add_items: ["0x456"], remove_items: ["0xabc"]) puts "updated list: added 0x456, removed 0xabc" diff --git a/ruby/examples/streams.rb b/ruby/examples/streams.rb index 1648271..6c6e362 100644 --- a/ruby/examples/streams.rb +++ b/ruby/examples/streams.rb @@ -1,4 +1,3 @@ -require "json" require_relative "../lib/quicknode_sdk" qn = QuicknodeSdk::SDK.from_env @@ -11,7 +10,7 @@ compression: "none" ) -stream_json = qn.streams.create_stream({ +stream = qn.streams.create_stream( name: "My Stream", network: "ethereum-mainnet", dataset: "block", @@ -27,7 +26,6 @@ keep_distance_from_tip: 0, elastic_batch_enabled: true, status: "active" -}) +) -stream = JSON.parse(stream_json) -puts "#{stream["id"]} | #{stream["name"]} | #{stream["status"]}" +puts "#{stream[:id]} | #{stream[:name]} | #{stream[:status]}" diff --git a/ruby/examples/streams_e2e.rb b/ruby/examples/streams_e2e.rb index 46f424b..6d83ef1 100644 --- a/ruby/examples/streams_e2e.rb +++ b/ruby/examples/streams_e2e.rb @@ -1,21 +1,27 @@ -require "json" require_relative "../lib/quicknode_sdk" qn = QuicknodeSdk::SDK.from_env -before = JSON.parse(qn.streams.list_streams({})) -puts "streams before: #{before["pageInfo"]["total"]}" +# Exercise all three call styles the dispatcher supports against arity-1 +# native read-only methods. list_streams/get_enabled_count must work bare, +# with kwargs, and with a positional empty hash. +before = qn.streams.list_streams({}) +puts "streams before: #{before.dig(:pageInfo, :total)}" +raise "list_streams bare broke" unless qn.streams.list_streams.keys.sort == before.keys.sort +raise "list_streams kwargs splat broke" unless qn.streams.list_streams(**{}).keys.sort == before.keys.sort -count = JSON.parse(qn.streams.get_enabled_count({})) -puts "enabled count: #{count["total"]}" +count = qn.streams.get_enabled_count({}) +puts "enabled count: #{count[:total]}" +raise "get_enabled_count bare broke" unless qn.streams.get_enabled_count.keys.sort == count.keys.sort +raise "get_enabled_count kwargs splat broke" unless qn.streams.get_enabled_count(**{}).keys.sort == count.keys.sort -filter_result = JSON.parse(qn.streams.test_filter( +filter_result = qn.streams.test_filter( network: "ethereum-mainnet", dataset: "block", block: "17811625", filter_function: "ZnVuY3Rpb24gbWFpbihkYXRhKSB7IHJldHVybiBkYXRhOyB9" -)) -puts "filter logs: #{filter_result["logs"]}" +) +puts "filter logs: #{filter_result[:logs]}" sleep 1 dest = QuicknodeSdk::DestinationAttributes.webhook( @@ -34,7 +40,7 @@ compression: "none" ) -stream = JSON.parse(qn.streams.create_stream({ +stream = qn.streams.create_stream( name: "E2E Test Stream", network: "ethereum-mainnet", dataset: "block", @@ -51,15 +57,15 @@ keep_distance_from_tip: 0, elastic_batch_enabled: true, status: "active" -})) -stream_id = stream["id"] -puts "created: #{stream_id} | #{stream["status"]}" +) +stream_id = stream[:id] +puts "created: #{stream_id} | #{stream[:status]}" -fetched = JSON.parse(qn.streams.get_stream(id: stream_id)) -puts "fetched: #{fetched["id"]} | #{fetched["name"]}" +fetched = qn.streams.get_stream(id: stream_id) +puts "fetched: #{fetched[:id]} | #{fetched[:name]}" -updated = JSON.parse(qn.streams.update_stream(id: stream_id, name: "E2E Test Stream Updated")) -puts "updated name: #{updated["name"]}" +updated = qn.streams.update_stream(id: stream_id, name: "E2E Test Stream Updated") +puts "updated name: #{updated[:name]}" sleep 1 qn.streams.pause_stream(id: stream_id) @@ -72,5 +78,5 @@ puts "deleted: #{stream_id}" sleep 1 -after = JSON.parse(qn.streams.list_streams({})) -puts "streams after: #{after["pageInfo"]["total"]}" +after = qn.streams.list_streams({}) +puts "streams after: #{after.dig(:pageInfo, :total)}" diff --git a/ruby/examples/webhooks_e2e.rb b/ruby/examples/webhooks_e2e.rb index d008f80..2af5cc8 100644 --- a/ruby/examples/webhooks_e2e.rb +++ b/ruby/examples/webhooks_e2e.rb @@ -3,11 +3,19 @@ qn = QuicknodeSdk::SDK.from_env -before = JSON.parse(qn.webhooks.list_webhooks({})) -puts "webhooks before: #{before["data"].length} (total=#{before["pageInfo"]["total"]})" - -count = JSON.parse(qn.webhooks.get_enabled_count) -puts "enabled count: #{count["total"]}" +# list_webhooks is arity-1 native: exercise bare, kwargs, and positional hash. +before = qn.webhooks.list_webhooks({}) +puts "webhooks before: #{before[:data].length} (total=#{before.dig(:pageInfo, :total)})" +raise "list_webhooks bare broke" unless qn.webhooks.list_webhooks.keys.sort == before.keys.sort +raise "list_webhooks kwargs splat broke" unless qn.webhooks.list_webhooks(**{}).keys.sort == before.keys.sort + +# get_enabled_count is arity-0 native: exercise bare, positional empty hash, +# and kwargs splat — all three must reach the no-args branch of the +# dispatcher cleanly. +count = qn.webhooks.get_enabled_count +puts "enabled count: #{count[:total]}" +raise "get_enabled_count positional hash broke" unless qn.webhooks.get_enabled_count({}).keys.sort == count.keys.sort +raise "get_enabled_count kwargs splat broke" unless qn.webhooks.get_enabled_count(**{}).keys.sort == count.keys.sort destination_attributes = JSON.generate({ url: "https://webhook.site/ae19071a-2dcc-4035-9cdf-406dcb4719ef", @@ -19,20 +27,20 @@ templateArgs: { wallets: ["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"] } }) -webhook = JSON.parse(qn.webhooks.create_webhook_from_template( +webhook = qn.webhooks.create_webhook_from_template( name: "E2E Test Webhook", network: "ethereum-mainnet", destination_attributes_json: destination_attributes, template_args_json: template_args -)) -id = webhook["id"] -puts "created: #{id} | #{webhook["status"]}" +) +id = webhook[:id] +puts "created: #{id} | #{webhook[:status]}" -fetched = JSON.parse(qn.webhooks.get_webhook(id: id)) -puts "fetched: #{fetched["id"]} | #{fetched["name"]}" +fetched = qn.webhooks.get_webhook(id: id) +puts "fetched: #{fetched[:id]} | #{fetched[:name]}" -updated = JSON.parse(qn.webhooks.update_webhook(id: id, name: "E2E Test Webhook Updated")) -puts "updated name: #{updated["name"]}" +updated = qn.webhooks.update_webhook(id: id, name: "E2E Test Webhook Updated") +puts "updated name: #{updated[:name]}" qn.webhooks.pause_webhook(id: id) puts "paused" @@ -44,5 +52,5 @@ puts "deleted: #{id}" sleep 1 -after = JSON.parse(qn.webhooks.list_webhooks({})) -puts "webhooks after: #{after["data"].length} (total=#{after["pageInfo"]["total"]})" +after = qn.webhooks.list_webhooks({}) +puts "webhooks after: #{after[:data].length} (total=#{after.dig(:pageInfo, :total)})" diff --git a/ruby/lib/quicknode_sdk.rb b/ruby/lib/quicknode_sdk.rb index ac2a35c..f1509ce 100644 --- a/ruby/lib/quicknode_sdk.rb +++ b/ruby/lib/quicknode_sdk.rb @@ -1 +1,8 @@ require_relative "quicknode_sdk.bundle" +require_relative "quicknode_sdk/wrap" +require_relative "quicknode_sdk/native_delegator" +require_relative "quicknode_sdk/clients/admin" +require_relative "quicknode_sdk/clients/streams" +require_relative "quicknode_sdk/clients/webhooks" +require_relative "quicknode_sdk/clients/kvstore" +require_relative "quicknode_sdk/sdk" diff --git a/ruby/lib/quicknode_sdk/clients/admin.rb b/ruby/lib/quicknode_sdk/clients/admin.rb new file mode 100644 index 0000000..dc74d59 --- /dev/null +++ b/ruby/lib/quicknode_sdk/clients/admin.rb @@ -0,0 +1,4 @@ +module QuicknodeSdk + class Admin < NativeDelegator + end +end diff --git a/ruby/lib/quicknode_sdk/clients/kvstore.rb b/ruby/lib/quicknode_sdk/clients/kvstore.rb new file mode 100644 index 0000000..879255e --- /dev/null +++ b/ruby/lib/quicknode_sdk/clients/kvstore.rb @@ -0,0 +1,4 @@ +module QuicknodeSdk + class KvStore < NativeDelegator + end +end diff --git a/ruby/lib/quicknode_sdk/clients/streams.rb b/ruby/lib/quicknode_sdk/clients/streams.rb new file mode 100644 index 0000000..c03f531 --- /dev/null +++ b/ruby/lib/quicknode_sdk/clients/streams.rb @@ -0,0 +1,4 @@ +module QuicknodeSdk + class Streams < NativeDelegator + end +end diff --git a/ruby/lib/quicknode_sdk/clients/webhooks.rb b/ruby/lib/quicknode_sdk/clients/webhooks.rb new file mode 100644 index 0000000..8b5ebc1 --- /dev/null +++ b/ruby/lib/quicknode_sdk/clients/webhooks.rb @@ -0,0 +1,4 @@ +module QuicknodeSdk + class Webhooks < NativeDelegator + end +end diff --git a/ruby/lib/quicknode_sdk/native_delegator.rb b/ruby/lib/quicknode_sdk/native_delegator.rb new file mode 100644 index 0000000..00b0b13 --- /dev/null +++ b/ruby/lib/quicknode_sdk/native_delegator.rb @@ -0,0 +1,43 @@ +module QuicknodeSdk + # Shared base class for the user-facing client wrappers (Admin, Streams, + # Webhooks, KvStore). Each Magnus-bound native client is held in @native and + # all method calls are forwarded through method_missing. + # + # The native side exposes two kinds of methods: arity-0 (e.g. list_chains) + # and arity-1 taking a single positional Hash of options (e.g. + # get_endpoints). To support all three Ruby call styles documented in the + # README and examples — bare (qn.admin.get_endpoints), kwargs + # (qn.admin.get_endpoints(limit: 5)), and positional hash + # (qn.streams.list_streams({})) — we coerce whatever the caller passed into + # a single options hash, then dispatch on the native arity. Arity-0 methods + # reject any argument (Magnus enforces this), so we must call them with no + # args; arity-1 methods always need a Hash passed POSITIONALLY (Magnus's + # RHash is a positional arg, and Ruby 3 treats `**{}` as zero arguments — + # so we must not splat the options). + class NativeDelegator + def initialize(native) + @native = native + end + + def method_missing(name, *args, **kwargs) + return super unless @native.respond_to?(name) + opts = if !kwargs.empty? + kwargs + elsif args.length == 1 && args[0].is_a?(Hash) + args[0] + else + {} + end + result = if @native.method(name).arity == 0 + @native.public_send(name) + else + @native.public_send(name, opts) + end + QuicknodeSdk.wrap(result) + end + + def respond_to_missing?(name, include_private = false) + @native.respond_to?(name, include_private) || super + end + end +end diff --git a/ruby/lib/quicknode_sdk/sdk.rb b/ruby/lib/quicknode_sdk/sdk.rb new file mode 100644 index 0000000..b6c218b --- /dev/null +++ b/ruby/lib/quicknode_sdk/sdk.rb @@ -0,0 +1,27 @@ +module QuicknodeSdk + class SDK + def self.from_env + new(Native::SDK.from_env) + end + + def initialize(native) + @native = native + end + + def admin + Admin.new(@native.admin) + end + + def streams + Streams.new(@native.streams) + end + + def webhooks + Webhooks.new(@native.webhooks) + end + + def kvstore + KvStore.new(@native.kvstore) + end + end +end diff --git a/ruby/lib/quicknode_sdk/wrap.rb b/ruby/lib/quicknode_sdk/wrap.rb new file mode 100644 index 0000000..9bf1cc3 --- /dev/null +++ b/ruby/lib/quicknode_sdk/wrap.rb @@ -0,0 +1,16 @@ +require "hashie" + +module QuicknodeSdk + class IndifferentHash < Hash + include Hashie::Extensions::MergeInitializer + include Hashie::Extensions::IndifferentAccess + end + + def self.wrap(v) + case v + when Hash then IndifferentHash.new(v).tap { |h| h.each { |k, val| h[k] = wrap(val) } } + when Array then v.map { |x| wrap(x) } + else v + end + end +end diff --git a/ruby/quicknode_sdk.gemspec b/ruby/quicknode_sdk.gemspec index 77d2b54..f27abc2 100644 --- a/ruby/quicknode_sdk.gemspec +++ b/ruby/quicknode_sdk.gemspec @@ -4,6 +4,7 @@ Gem::Specification.new do |s| s.summary = "Quicknode SDK for Ruby" s.authors = ["Quicknode"] s.license = "MIT" - s.files = ["lib/quicknode_sdk.rb"] + s.files = Dir["lib/**/*.rb"] + Dir["sig/**/*.rbs"] + ["lib/quicknode_sdk.bundle"] s.required_ruby_version = ">= 3.0" + s.add_runtime_dependency "hashie", "~> 5.0" end diff --git a/ruby/sig/quicknode_sdk.rbs b/ruby/sig/quicknode_sdk.rbs new file mode 100644 index 0000000..b562352 --- /dev/null +++ b/ruby/sig/quicknode_sdk.rbs @@ -0,0 +1,162 @@ +module QuicknodeSdk + def self.wrap: (untyped v) -> untyped + + class Error < StandardError + end + + class ConfigError < Error + end + + class HttpError < Error + end + + class TimeoutError < HttpError + end + + class ConnectionError < HttpError + end + + class ApiError < Error + attr_reader status: Integer + attr_reader body: String + end + + class DecodeError < Error + attr_reader body: String + end + + class SDK + def self.from_env: () -> SDK + def initialize: (untyped native) -> void + def admin: () -> Admin + def streams: () -> Streams + def webhooks: () -> Webhooks + def kvstore: () -> KvStore + end + + class DestinationAttributes + def self.webhook: (url: String, ?max_retry: Integer, ?retry_interval_sec: Integer, ?post_timeout_sec: Integer, ?security_token: String, compression: String) -> DestinationAttributes + def self.s3: (endpoint: String, access_key: String, secret_key: String, bucket: String, object_prefix: String, compression: String, file_type: String, ?max_retry: Integer, ?retry_interval_sec: Integer, ?use_ssl: bool) -> DestinationAttributes + def self.azure: (storage_account: String, sas_token: String, container: String, compression: String, file_type: String, ?max_retry: Integer, ?retry_interval_sec: Integer, ?blob_prefix: String) -> DestinationAttributes + def self.postgres: (host: String, ?port: Integer, database: String, username: String, password: String, table_name: String, sslmode: String, ?max_retry: Integer, ?retry_interval_sec: Integer) -> DestinationAttributes + def self.mysql: (host: String, ?port: Integer, database: String, username: String, password: String, table_name: String, ?max_retry: Integer, ?retry_interval_sec: Integer) -> DestinationAttributes + def self.mongo: (host: String, database: String, username: String, password: String, collection_name: String, ?max_retry: Integer, ?retry_interval_sec: Integer) -> DestinationAttributes + def self.clickhouse: (hosts: String, database: String, username: String, password: String, table_name: String, default_table_engine_opts: String, ?default_granularity: Integer, default_compression: String, default_index_type: String, ?max_retry: Integer, ?retry_interval_sec: Integer, ?disable_datetime_precision: bool, ?dont_support_rename_column: bool, ?dont_support_empty_default_value: bool, ?skip_initialize_with_version: bool) -> DestinationAttributes + def self.snowflake: (account: String, host: String, ?port: Integer, protocol: String, database: String, schema: String, warehouse: String, username: String, password: String, ?max_retry: Integer, ?retry_interval_sec: Integer, ?table_name: String) -> DestinationAttributes + def self.kafka: (bootstrap_servers: String, topic_name: String, compression_type: String, ?batch_size: Integer, ?linger_ms: Integer, ?max_request_size: Integer, ?timeout_sec: Integer, ?max_retry: Integer, ?retry_interval_sec: Integer, ?username: String, ?password: String, ?protocol: String, ?mechanisms: String) -> DestinationAttributes + def self.redis: (host: String, ?port: Integer, ?database: Integer, username: String, password: String, key_name: String, ?max_retry: Integer, ?retry_interval_sec: Integer, ?tls: bool) -> DestinationAttributes + end + + class Admin + def initialize: (untyped native) -> void + + def get_endpoints: (?limit: Integer, ?offset: Integer, ?search: String, ?sort_by: String, ?sort_direction: String, ?networks: Array[String], ?statuses: Array[String], ?labels: Array[String], ?dedicated: bool, ?is_flat_rate: bool, ?tag_ids: Array[Integer], ?tag_labels: Array[String]) -> untyped + def create_endpoint: (?chain: String, ?network: String) -> untyped + def show_endpoint: (id: String) -> untyped + def update_endpoint: (id: String, ?label: String) -> void + def archive_endpoint: (id: String) -> void + def update_endpoint_status: (id: String, status: String) -> untyped + def create_tag: (id: String, ?label: String) -> void + def delete_tag: (id: String, tag_id: String) -> void + def get_usage: (?start_time: Integer, ?end_time: Integer) -> untyped + def get_usage_by_endpoint: (?start_time: Integer, ?end_time: Integer) -> untyped + def get_usage_by_method: (?start_time: Integer, ?end_time: Integer) -> untyped + def get_usage_by_chain: (?start_time: Integer, ?end_time: Integer) -> untyped + def get_endpoint_logs: (id: String, from_time: String, to_time: String, ?include_details: bool, ?limit: Integer, ?next_at: String) -> untyped + def get_log_details: (id: String, request_id: String) -> untyped + def get_security_options: (id: String) -> untyped + def update_security_options: (id: String, ?tokens: String, ?referrers: String, ?jwts: String, ?ips: String, ?domain_masks: String, ?hsts: String, ?cors: String, ?request_filters: String, ?ip_custom_header: String) -> untyped + def create_token: (id: String) -> void + def delete_token: (id: String, token_id: String) -> untyped + def create_referrer: (id: String, ?referrer: String) -> void + def delete_referrer: (id: String, referrer_id: String) -> untyped + def create_ip: (id: String, ?ip: String) -> void + def delete_ip: (id: String, ip_id: String) -> untyped + def create_domain_mask: (id: String, ?domain_mask: String) -> void + def delete_domain_mask: (id: String, domain_mask_id: String) -> untyped + def create_jwt: (id: String, ?public_key: String, ?kid: String, ?name: String) -> void + def delete_jwt: (id: String, jwt_id: String) -> void + def create_request_filter: (id: String, ?methods: Array[String]) -> untyped + def update_request_filter: (id: String, request_filter_id: String, ?methods: Array[String]) -> void + def delete_request_filter: (id: String, request_filter_id: String) -> void + def enable_multichain: (id: String) -> void + def disable_multichain: (id: String) -> void + def create_or_update_ip_custom_header: (id: String, header_name: String) -> untyped + def delete_ip_custom_header: (id: String) -> untyped + def get_method_rate_limits: (id: String) -> untyped + def create_method_rate_limit: (id: String, interval: String, methods: Array[String], rate: Integer) -> untyped + def update_method_rate_limit: (id: String, method_rate_limit_id: String, ?methods: Array[String], ?status: String, ?rate: Integer) -> untyped + def delete_method_rate_limit: (id: String, method_rate_limit_id: String) -> void + def update_rate_limits: (id: String, ?rps: Integer, ?rpm: Integer, ?rpd: Integer) -> void + def get_endpoint_metrics: (id: String, period: String, metric: String) -> untyped + def get_account_metrics: (period: String, metric: String, ?percentile: String) -> untyped + def list_chains: () -> untyped + def list_invoices: () -> untyped + def list_payments: () -> untyped + def list_teams: () -> untyped + def create_team: (name: String) -> untyped + def get_team: (id: Integer) -> untyped + def delete_team: (id: Integer) -> untyped + def list_team_endpoints: (id: Integer) -> untyped + def update_team_endpoints: (id: Integer, endpoint_ids: Array[String]) -> untyped + def invite_team_member: (id: Integer, email: String, ?full_name: String, ?role: String) -> untyped + def remove_team_member: (id: Integer, user_id: Integer, ?destroy_user: bool) -> untyped + def resend_team_invite: (id: Integer, user_id: Integer) -> untyped + def bulk_update_endpoint_status: (ids: Array[String], status: String) -> untyped + def bulk_add_tag: (ids: Array[String], label: String) -> untyped + def bulk_remove_tag: (ids: Array[String], tag_id: Integer) -> untyped + def list_tags: () -> untyped + def rename_tag: (id: Integer, label: String) -> untyped + def delete_account_tag: (id: Integer) -> untyped + def get_usage_by_tag: (?start_time: Integer, ?end_time: Integer) -> untyped + def get_endpoint_security: (id: String) -> untyped + end + + class Streams + def initialize: (untyped native) -> void + + def create_stream: (name: String, network: String, dataset: String, region: String, start_range: Integer, end_range: Integer, destination_attributes: DestinationAttributes, plan: String, threshold_fetch_buffer: Integer, ?dataset_batch_size: Integer, ?max_batch_size: Integer, ?max_buffer_range_size: Integer, ?max_buffer_processing_workers: Integer, ?keep_distance_from_tip: Integer, ?filter_function: String, ?filter_language: String, ?include_stream_metadata: String, ?product_type: String, ?status: String, ?notification_email: String, ?charge_min_cap: Integer, ?fix_block_reorgs: Integer, ?elastic_batch_enabled: bool, ?extra_destinations: Array[DestinationAttributes]) -> untyped + def list_streams: (?stream_type: String, ?offset: Integer, ?limit: Integer, ?order_by: String, ?order_direction: String) -> untyped + def delete_all_streams: () -> void + def get_stream: (id: String) -> untyped + def update_stream: (id: String, ?name: String, ?network: String, ?dataset: String, ?region: String, ?start_range: Integer, ?end_range: Integer, ?destination_attributes: DestinationAttributes, ?plan: String, ?threshold_fetch_buffer: Integer, ?dataset_batch_size: Integer, ?max_batch_size: Integer, ?max_buffer_range_size: Integer, ?max_buffer_processing_workers: Integer, ?keep_distance_from_tip: Integer, ?filter_function: String, ?filter_language: String, ?include_stream_metadata: String, ?notification_email: String, ?charge_min_cap: Integer, ?fix_block_reorgs: Integer, ?elastic_batch_enabled: bool, ?status: String, ?memo: String, ?extra_destinations: Array[DestinationAttributes]) -> untyped + def delete_stream: (id: String) -> void + def activate_stream: (id: String) -> void + def pause_stream: (id: String) -> void + def test_filter: (network: String, dataset: String, block: String, ?filter_function: String, ?filter_language: String) -> untyped + def get_enabled_count: (?stream_type: String) -> untyped + end + + class Webhooks + def initialize: (untyped native) -> void + + def list_webhooks: (?limit: Integer, ?offset: Integer) -> untyped + def delete_all_webhooks: () -> void + def get_webhook: (id: String) -> untyped + def update_webhook: (id: String, ?name: String, ?notification_email: String, ?destination_attributes_json: String) -> untyped + def delete_webhook: (id: String) -> void + def pause_webhook: (id: String) -> void + def activate_webhook: (id: String, start_from: String) -> void + def get_enabled_count: () -> untyped + def create_webhook_from_template: (name: String, network: String, destination_attributes_json: String, template_args_json: String, ?notification_email: String) -> untyped + def update_webhook_template: (webhook_id: String, template_args_json: String, ?name: String, ?notification_email: String) -> untyped + end + + class KvStore + def initialize: (untyped native) -> void + + def create_set: (key: String, value: String) -> void + def get_sets: (?limit: Integer, ?cursor: String) -> untyped + def get_set: (key: String) -> untyped + def bulk_sets: (?add_sets: Hash[String, String], ?delete_sets: Array[String]) -> void + def delete_set: (key: String) -> void + def create_list: (key: String, items: Array[String]) -> void + def get_lists: (?limit: Integer, ?cursor: String) -> untyped + def get_list: (key: String, ?limit: Integer, ?cursor: String) -> untyped + def update_list: (key: String, ?add_items: Array[String], ?remove_items: Array[String]) -> void + def add_list_item: (key: String, item: String) -> void + def list_contains_item: (key: String, item: String) -> untyped + def delete_list_item: (key: String, item: String) -> void + def delete_list: (key: String) -> void + end +end