squirreldb-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a43ce4beedd5703705fb80885a05a914a7b46466cf6a2503629bbe2f6c49ade7
4
+ data.tar.gz: cab0bcc0c3e779169a5ec73c60ad5ba32b3d957c5a566205303ed768563b5915
5
+ SHA512:
6
+ metadata.gz: d7152d7f43378e2abffcbf77b1309abd276e13a37759dc70e48bd19ded705ad32de31fdf2b17e6673360d9ce1709d7e408bbdc34f5656e09116e9d135b55f3d3
7
+ data.tar.gz: 0b36d4f5f4e8a1f6141113eb768264ccd2c4c7079d448892f8cb497e10b3f164ff5456ea418d72cf0b3cba343a86985489a975c87a9b97ab645dbf344f1b87fc
@@ -0,0 +1,163 @@
1
+ # SquirrelDB Ruby SDK - Cache (Redis-compatible)
2
+ # Generated by sdk-generator v0.1.0
3
+ # DO NOT EDIT MANUALLY
4
+
5
+ # frozen_string_literal: true
6
+
7
+ require "socket"
8
+
9
+ module SquirrelDB
10
+ # Redis-compatible cache client using RESP protocol
11
+ class Cache
12
+ class Error < StandardError; end
13
+
14
+ def initialize(host: "localhost", port: 6379)
15
+ @host = host
16
+ @port = port
17
+ @socket = nil
18
+ end
19
+
20
+ def self.connect(host: "localhost", port: 6379)
21
+ cache = new(host: host, port: port)
22
+ cache.send(:do_connect)
23
+ cache
24
+ end
25
+
26
+ def get(key)
27
+ resp = command("GET", key)
28
+ resp.is_a?(NilClass) ? nil : resp
29
+ end
30
+
31
+ def set(key, value, ttl: nil)
32
+ if ttl
33
+ command("SET", key, value, "EX", ttl.to_s)
34
+ else
35
+ command("SET", key, value)
36
+ end
37
+ true
38
+ end
39
+
40
+ def del(key)
41
+ command("DEL", key) > 0
42
+ end
43
+
44
+ def exists(key)
45
+ command("EXISTS", key) > 0
46
+ end
47
+
48
+ def expire(key, seconds)
49
+ command("EXPIRE", key, seconds.to_s) > 0
50
+ end
51
+
52
+ def ttl(key)
53
+ command("TTL", key)
54
+ end
55
+
56
+ def incr(key)
57
+ command("INCR", key)
58
+ end
59
+
60
+ def decr(key)
61
+ command("DECR", key)
62
+ end
63
+
64
+ def incrby(key, amount)
65
+ command("INCRBY", key, amount.to_s)
66
+ end
67
+
68
+ def decrby(key, amount)
69
+ command("DECRBY", key, amount.to_s)
70
+ end
71
+
72
+ def mget(*keys)
73
+ command("MGET", *keys)
74
+ end
75
+
76
+ def mset(hash)
77
+ args = hash.flat_map { |k, v| [k.to_s, v.to_s] }
78
+ command("MSET", *args)
79
+ true
80
+ end
81
+
82
+ def keys(pattern = "*")
83
+ command("KEYS", pattern)
84
+ end
85
+
86
+ def dbsize
87
+ command("DBSIZE")
88
+ end
89
+
90
+ def flushdb
91
+ command("FLUSHDB")
92
+ true
93
+ end
94
+
95
+ def info
96
+ result = command("INFO")
97
+ result.split("\n").reject { |l| l.empty? || l.start_with?("#") }.map do |line|
98
+ line.split(":", 2)
99
+ end.to_h
100
+ end
101
+
102
+ def ping
103
+ command("PING") == "PONG"
104
+ end
105
+
106
+ def close
107
+ command("QUIT") rescue nil
108
+ @socket&.close
109
+ @socket = nil
110
+ end
111
+
112
+ private
113
+
114
+ def do_connect
115
+ @socket = TCPSocket.new(@host, @port)
116
+ end
117
+
118
+ def command(*args)
119
+ raise Error, "Not connected" unless @socket
120
+
121
+ @socket.write(encode_command(args))
122
+ read_response
123
+ end
124
+
125
+ def encode_command(args)
126
+ parts = ["*#{args.length}\r\n"]
127
+ args.each do |arg|
128
+ arg = arg.to_s
129
+ parts << "$#{arg.bytesize}\r\n#{arg}\r\n"
130
+ end
131
+ parts.join
132
+ end
133
+
134
+ def read_response
135
+ line = @socket.gets.chomp
136
+ prefix = line[0]
137
+ content = line[1..]
138
+
139
+ case prefix
140
+ when "+"
141
+ content
142
+ when "-"
143
+ raise Error, content
144
+ when ":"
145
+ content.to_i
146
+ when "$"
147
+ len = content.to_i
148
+ return nil if len == -1
149
+
150
+ data = @socket.read(len)
151
+ @socket.read(2) # Read \r\n
152
+ data
153
+ when "*"
154
+ count = content.to_i
155
+ return nil if count == -1
156
+
157
+ count.times.map { read_response }
158
+ else
159
+ raise Error, "Unknown RESP type: #{prefix}"
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,202 @@
1
+ # SquirrelDB Ruby SDK - Client
2
+ # Generated by sdk-generator v0.1.0
3
+ # DO NOT EDIT MANUALLY
4
+
5
+ # frozen_string_literal: true
6
+
7
+ require "json"
8
+ require "securerandom"
9
+ require "websocket-client-simple"
10
+ require "thread"
11
+
12
+ module SquirrelDB
13
+ # Client for connecting to SquirrelDB
14
+ class Client
15
+ def initialize(url, reconnect: true, max_reconnect_attempts: 10, reconnect_delay: 1.0)
16
+ @url = url.start_with?("ws://", "wss://") ? url : "ws://#{url}"
17
+ @reconnect = reconnect
18
+ @max_reconnect_attempts = max_reconnect_attempts
19
+ @reconnect_delay = reconnect_delay
20
+ @pending = {}
21
+ @subscriptions = {}
22
+ @mutex = Mutex.new
23
+ @closed = false
24
+ @reconnect_attempts = 0
25
+ end
26
+
27
+ def self.connect(url, **options)
28
+ client = new(url, **options)
29
+ client.send(:do_connect)
30
+ client
31
+ end
32
+
33
+ def query(q)
34
+ resp = send_message({ type: "query", id: generate_id, query: q })
35
+ raise resp["error"] if resp["type"] == "error"
36
+ resp["data"].map { |d| Document.from_hash(d) }
37
+ end
38
+
39
+ def subscribe(table_name)
40
+ SubscriptionBuilder.new(self, table_name)
41
+ end
42
+
43
+ def subscribe_raw(q, &callback)
44
+ sub_id = generate_id
45
+ resp = send_message({ type: "subscribe", id: sub_id, query: q })
46
+ raise resp["error"] if resp["type"] == "error"
47
+ @mutex.synchronize { @subscriptions[sub_id] = callback }
48
+ sub_id
49
+ end
50
+
51
+ def unsubscribe(subscription_id)
52
+ send_message({ type: "unsubscribe", id: subscription_id })
53
+ @mutex.synchronize { @subscriptions.delete(subscription_id) }
54
+ nil
55
+ end
56
+
57
+ def insert(collection, data)
58
+ resp = send_message({
59
+ type: "insert",
60
+ id: generate_id,
61
+ collection: collection,
62
+ data: data
63
+ })
64
+ raise resp["error"] if resp["type"] == "error"
65
+ Document.from_hash(resp["data"])
66
+ end
67
+
68
+ def update(collection, document_id, data)
69
+ resp = send_message({
70
+ type: "update",
71
+ id: generate_id,
72
+ collection: collection,
73
+ document_id: document_id,
74
+ data: data
75
+ })
76
+ raise resp["error"] if resp["type"] == "error"
77
+ Document.from_hash(resp["data"])
78
+ end
79
+
80
+ def delete(collection, document_id)
81
+ resp = send_message({
82
+ type: "delete",
83
+ id: generate_id,
84
+ collection: collection,
85
+ document_id: document_id
86
+ })
87
+ raise resp["error"] if resp["type"] == "error"
88
+ Document.from_hash(resp["data"])
89
+ end
90
+
91
+ def list_collections
92
+ resp = send_message({ type: "listcollections", id: generate_id })
93
+ raise resp["error"] if resp["type"] == "error"
94
+ resp["data"]
95
+ end
96
+
97
+ def ping
98
+ resp = send_message({ type: "ping", id: generate_id })
99
+ raise "Unexpected response" unless resp["type"] == "pong"
100
+ nil
101
+ end
102
+
103
+ def close
104
+ @closed = true
105
+ @mutex.synchronize { @subscriptions.clear }
106
+ @ws&.close
107
+ end
108
+
109
+ private
110
+
111
+ def do_connect
112
+ @ws = WebSocket::Client::Simple.connect(@url)
113
+ client = self
114
+
115
+ @ws.on :message do |msg|
116
+ client.send(:handle_message, msg.data)
117
+ end
118
+
119
+ @ws.on :close do
120
+ client.send(:handle_disconnect)
121
+ end
122
+
123
+ @ws.on :error do |e|
124
+ # Handle error silently
125
+ end
126
+
127
+ sleep 0.1 until @ws.open?
128
+ @reconnect_attempts = 0
129
+ end
130
+
131
+ def handle_message(data)
132
+ msg = JSON.parse(data)
133
+ msg_type = msg["type"]
134
+ msg_id = msg["id"]
135
+
136
+ if msg_type == "change"
137
+ callback = @mutex.synchronize { @subscriptions[msg_id] }
138
+ callback&.call(ChangeEvent.from_hash(msg["change"]))
139
+ return
140
+ end
141
+
142
+ queue = @mutex.synchronize { @pending.delete(msg_id) }
143
+ queue&.push(msg)
144
+ rescue JSON::ParserError
145
+ # Ignore malformed messages
146
+ end
147
+
148
+ def handle_disconnect
149
+ return if @closed
150
+
151
+ @mutex.synchronize do
152
+ @pending.each_value { |q| q.push({ "type" => "error", "error" => "Connection closed" }) }
153
+ @pending.clear
154
+ end
155
+
156
+ if @reconnect && @reconnect_attempts < @max_reconnect_attempts
157
+ @reconnect_attempts += 1
158
+ delay = @reconnect_delay * (2 ** (@reconnect_attempts - 1))
159
+ sleep delay
160
+ do_connect rescue nil
161
+ end
162
+ end
163
+
164
+ def send_message(msg)
165
+ raise "Not connected" unless @ws&.open?
166
+
167
+ queue = Queue.new
168
+ @mutex.synchronize { @pending[msg[:id]] = queue }
169
+
170
+ @ws.send(msg.to_json)
171
+ queue.pop
172
+ end
173
+
174
+ def generate_id
175
+ SecureRandom.uuid
176
+ end
177
+ end
178
+
179
+ # Subscription builder for fluent change subscriptions
180
+ class SubscriptionBuilder
181
+ def initialize(client, table_name)
182
+ @client = client
183
+ @table_name = table_name
184
+ @filter_condition = nil
185
+ end
186
+
187
+ def find(condition = nil, &block)
188
+ @filter_condition = block_given? ? block.call(Query::DocProxy.new) : condition
189
+ self
190
+ end
191
+
192
+ def changes(&callback)
193
+ query = { "table" => @table_name, "changes" => { "includeInitial" => false } }
194
+ query["filter"] = Query.filter_to_structured(@filter_condition) if @filter_condition
195
+ @client.subscribe_structured(query, &callback)
196
+ end
197
+ end
198
+
199
+ def self.connect(url, **options)
200
+ Client.connect(url, **options)
201
+ end
202
+ end
@@ -0,0 +1,177 @@
1
+ # SquirrelDB Ruby SDK - Storage (S3-compatible)
2
+ # Generated by sdk-generator v0.1.0
3
+ # DO NOT EDIT MANUALLY
4
+
5
+ # frozen_string_literal: true
6
+
7
+ require "net/http"
8
+ require "uri"
9
+ require "openssl"
10
+ require "time"
11
+ require "rexml/document"
12
+
13
+ module SquirrelDB
14
+ # S3-compatible storage client
15
+ class Storage
16
+ class Error < StandardError
17
+ attr_reader :status_code
18
+
19
+ def initialize(message, status_code = nil)
20
+ super(message)
21
+ @status_code = status_code
22
+ end
23
+ end
24
+
25
+ Bucket = Struct.new(:name, :created_at, keyword_init: true)
26
+ StorageObject = Struct.new(:key, :size, :etag, :last_modified, keyword_init: true)
27
+
28
+ def initialize(endpoint:, access_key: nil, secret_key: nil, region: "us-east-1")
29
+ @endpoint = endpoint.chomp("/")
30
+ @access_key = access_key
31
+ @secret_key = secret_key
32
+ @region = region
33
+ end
34
+
35
+ def list_buckets
36
+ response = request("GET", "/")
37
+ doc = REXML::Document.new(response.body)
38
+ doc.elements.collect("//Bucket/Name") do |e|
39
+ Bucket.new(name: e.text, created_at: Time.now)
40
+ end
41
+ end
42
+
43
+ def create_bucket(name)
44
+ request("PUT", "/#{name}")
45
+ true
46
+ end
47
+
48
+ def delete_bucket(name)
49
+ request("DELETE", "/#{name}")
50
+ true
51
+ end
52
+
53
+ def bucket_exists?(name)
54
+ request("HEAD", "/#{name}")
55
+ true
56
+ rescue Error
57
+ false
58
+ end
59
+
60
+ def list_objects(bucket, prefix: nil, max_keys: nil)
61
+ path = "/#{bucket}"
62
+ params = []
63
+ params << "prefix=#{URI.encode_www_form_component(prefix)}" if prefix
64
+ params << "max-keys=#{max_keys}" if max_keys
65
+ path += "?#{params.join("&")}" unless params.empty?
66
+
67
+ response = request("GET", path)
68
+ doc = REXML::Document.new(response.body)
69
+
70
+ doc.elements.collect("//Contents") do |e|
71
+ StorageObject.new(
72
+ key: e.elements["Key"]&.text,
73
+ size: e.elements["Size"]&.text&.to_i || 0,
74
+ etag: e.elements["ETag"]&.text&.gsub('"', ""),
75
+ last_modified: Time.now
76
+ )
77
+ end
78
+ end
79
+
80
+ def get_object(bucket, key)
81
+ response = request("GET", "/#{bucket}/#{key}")
82
+ response.body
83
+ end
84
+
85
+ def put_object(bucket, key, data, content_type: "application/octet-stream")
86
+ headers = { "Content-Type" => content_type }
87
+ response = request("PUT", "/#{bucket}/#{key}", body: data, headers: headers)
88
+ response["etag"]&.gsub('"', "") || ""
89
+ end
90
+
91
+ def delete_object(bucket, key)
92
+ request("DELETE", "/#{bucket}/#{key}")
93
+ true
94
+ end
95
+
96
+ def object_exists?(bucket, key)
97
+ request("HEAD", "/#{bucket}/#{key}")
98
+ true
99
+ rescue Error
100
+ false
101
+ end
102
+
103
+ private
104
+
105
+ def request(method, path, body: nil, headers: {})
106
+ uri = URI.parse("#{@endpoint}#{path}")
107
+ http = Net::HTTP.new(uri.host, uri.port)
108
+ http.use_ssl = uri.scheme == "https"
109
+
110
+ request_class = {
111
+ "GET" => Net::HTTP::Get,
112
+ "PUT" => Net::HTTP::Put,
113
+ "DELETE" => Net::HTTP::Delete,
114
+ "HEAD" => Net::HTTP::Head
115
+ }[method]
116
+
117
+ req = request_class.new(uri)
118
+ headers.each { |k, v| req[k] = v }
119
+ req.body = body if body
120
+
121
+ if @access_key && @secret_key
122
+ sign_request(req, uri, body)
123
+ end
124
+
125
+ response = http.request(req)
126
+
127
+ unless response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPNoContent)
128
+ raise Error.new(response.body, response.code.to_i)
129
+ end
130
+
131
+ response
132
+ end
133
+
134
+ def sign_request(req, uri, body)
135
+ now = Time.now.utc
136
+ date_stamp = now.strftime("%Y%m%d")
137
+ amz_date = now.strftime("%Y%m%dT%H%M%SZ")
138
+
139
+ payload_hash = OpenSSL::Digest::SHA256.hexdigest(body || "")
140
+ req["x-amz-date"] = amz_date
141
+ req["x-amz-content-sha256"] = payload_hash
142
+
143
+ signed_headers = (req.to_hash.keys + ["host"]).sort.join(";")
144
+ canonical_headers = (req.to_hash.merge("host" => [uri.host])).sort.map { |k, v| "#{k}:#{v.join(",")}\n" }.join
145
+
146
+ canonical_request = [
147
+ req.method,
148
+ uri.path,
149
+ uri.query || "",
150
+ canonical_headers,
151
+ signed_headers,
152
+ payload_hash
153
+ ].join("\n")
154
+
155
+ algorithm = "AWS4-HMAC-SHA256"
156
+ credential_scope = "#{date_stamp}/#{@region}/s3/aws4_request"
157
+ string_to_sign = [
158
+ algorithm,
159
+ amz_date,
160
+ credential_scope,
161
+ OpenSSL::Digest::SHA256.hexdigest(canonical_request)
162
+ ].join("\n")
163
+
164
+ k_date = hmac_sha256("AWS4#{@secret_key}", date_stamp)
165
+ k_region = hmac_sha256(k_date, @region)
166
+ k_service = hmac_sha256(k_region, "s3")
167
+ k_signing = hmac_sha256(k_service, "aws4_request")
168
+ signature = OpenSSL::HMAC.hexdigest("SHA256", k_signing, string_to_sign)
169
+
170
+ req["Authorization"] = "#{algorithm} Credential=#{@access_key}/#{credential_scope}, SignedHeaders=#{signed_headers}, Signature=#{signature}"
171
+ end
172
+
173
+ def hmac_sha256(key, data)
174
+ OpenSSL::HMAC.digest("SHA256", key, data)
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,8 @@
1
+ # SquirrelDB Ruby SDK - Types
2
+ # Generated by sdk-generator v0.1.0
3
+ # DO NOT EDIT MANUALLY
4
+
5
+ # frozen_string_literal: true
6
+
7
+ module SquirrelDB
8
+ end
data/lib/squirreldb.rb ADDED
@@ -0,0 +1,14 @@
1
+ # SquirrelDB Ruby SDK
2
+ # Generated by sdk-generator v0.1.0
3
+ # DO NOT EDIT MANUALLY
4
+
5
+ # frozen_string_literal: true
6
+
7
+ require_relative "squirreldb/types"
8
+ require_relative "squirreldb/client"
9
+ require_relative "squirreldb/storage"
10
+ require_relative "squirreldb/cache"
11
+
12
+ module SquirrelDB
13
+ VERSION = "0.1.0"
14
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: squirreldb-sdk
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - SquirrelDB Contributors
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2026-01-29 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: websocket-client-simple
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.9'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.9'
26
+ - !ruby/object:Gem::Dependency
27
+ name: json
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '13.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '13.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: minitest
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '5.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '5.0'
68
+ description: A Ruby client for connecting to SquirrelDB realtime database
69
+ executables: []
70
+ extensions: []
71
+ extra_rdoc_files: []
72
+ files:
73
+ - lib/squirreldb.rb
74
+ - lib/squirreldb/cache.rb
75
+ - lib/squirreldb/client.rb
76
+ - lib/squirreldb/storage.rb
77
+ - lib/squirreldb/types.rb
78
+ homepage: ''
79
+ licenses:
80
+ - MIT
81
+ metadata: {}
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: 3.0.0
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubygems_version: 3.6.2
97
+ specification_version: 4
98
+ summary: Ruby client for SquirrelDB
99
+ test_files: []