seekmodo-sdk 0.5.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 +7 -0
- data/.github/workflows/ci.yml +52 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.rubocop.yml +32 -0
- data/Gemfile +11 -0
- data/README.md +135 -0
- data/docs/tool-catalog.README.md +12 -0
- data/lib/seekmodo/sdk/admin/client.rb +120 -0
- data/lib/seekmodo/sdk/auto_promoter.rb +124 -0
- data/lib/seekmodo/sdk/browser_token.rb +62 -0
- data/lib/seekmodo/sdk/circuit_breaker.rb +123 -0
- data/lib/seekmodo/sdk/connector/client.rb +250 -0
- data/lib/seekmodo/sdk/events/click_beacon.rb +58 -0
- data/lib/seekmodo/sdk/events/events_queue.rb +50 -0
- data/lib/seekmodo/sdk/exceptions/breaker_open_error.rb +10 -0
- data/lib/seekmodo/sdk/exceptions/client_error.rb +66 -0
- data/lib/seekmodo/sdk/exceptions/over_quota_error.rb +10 -0
- data/lib/seekmodo/sdk/exceptions/seekmodo_error.rb +8 -0
- data/lib/seekmodo/sdk/exceptions/signature_mismatch_error.rb +10 -0
- data/lib/seekmodo/sdk/exceptions/tenant_unavailable_error.rb +10 -0
- data/lib/seekmodo/sdk/hmac_signer.rb +43 -0
- data/lib/seekmodo/sdk/mcp/client.rb +105 -0
- data/lib/seekmodo/sdk/mode.rb +41 -0
- data/lib/seekmodo/sdk/mode_fsm.rb +52 -0
- data/lib/seekmodo/sdk/pairing.rb +114 -0
- data/lib/seekmodo/sdk/signature_mismatch_tracker.rb +52 -0
- data/lib/seekmodo/sdk/storage/memory/stores.rb +100 -0
- data/lib/seekmodo/sdk/storage/protocols.rb +47 -0
- data/lib/seekmodo/sdk/storefront/client.rb +71 -0
- data/lib/seekmodo/sdk/storefront/transport.rb +198 -0
- data/lib/seekmodo/sdk/tenant_snapshot.rb +65 -0
- data/lib/seekmodo/sdk/tools/registry.rb +88 -0
- data/lib/seekmodo/sdk/version.rb +7 -0
- data/lib/seekmodo/sdk.rb +33 -0
- data/lib/seekmodo-sdk.rb +3 -0
- metadata +109 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "faraday"
|
|
5
|
+
|
|
6
|
+
require_relative "../exceptions/seekmodo_error"
|
|
7
|
+
require_relative "../hmac_signer"
|
|
8
|
+
|
|
9
|
+
module Seekmodo
|
|
10
|
+
module Sdk
|
|
11
|
+
module Mcp
|
|
12
|
+
class Client
|
|
13
|
+
DEFAULT_GATEWAY_URL = "https://mcp.seekmodo.com"
|
|
14
|
+
|
|
15
|
+
def initialize(
|
|
16
|
+
gateway_url: DEFAULT_GATEWAY_URL,
|
|
17
|
+
signer: nil,
|
|
18
|
+
operator_token: nil,
|
|
19
|
+
tenant_id: nil,
|
|
20
|
+
connection: nil,
|
|
21
|
+
user_agent: "seekmodo-ruby-sdk/0.5.0"
|
|
22
|
+
)
|
|
23
|
+
@gateway_url = gateway_url.to_s.delete_suffix("/")
|
|
24
|
+
@signer = signer
|
|
25
|
+
@operator_token = operator_token
|
|
26
|
+
@tenant_id = tenant_id
|
|
27
|
+
@user_agent = user_agent
|
|
28
|
+
@connection = connection || Faraday.new do |f|
|
|
29
|
+
f.adapter Faraday.default_adapter
|
|
30
|
+
end
|
|
31
|
+
@request_id = 0
|
|
32
|
+
|
|
33
|
+
if @signer.nil? && @operator_token.nil?
|
|
34
|
+
raise ArgumentError, "MCP client requires signer (HMAC) or operator_token (bearer)"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def initialize_session(params = {})
|
|
39
|
+
rpc("initialize", params)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def tools_list
|
|
43
|
+
rpc("tools/list", {})
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def tools_call(name, arguments = {})
|
|
47
|
+
rpc("tools/call", { "name" => name, "arguments" => arguments })
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def ping
|
|
51
|
+
rpc("ping", {})
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def rpc(method, params)
|
|
57
|
+
@request_id += 1
|
|
58
|
+
envelope = {
|
|
59
|
+
"jsonrpc" => "2.0",
|
|
60
|
+
"id" => @request_id,
|
|
61
|
+
"method" => method,
|
|
62
|
+
"params" => params
|
|
63
|
+
}
|
|
64
|
+
body = JSON.generate(envelope)
|
|
65
|
+
response = @connection.post(@gateway_url + "/mcp") do |req|
|
|
66
|
+
req.headers.update(build_headers(body))
|
|
67
|
+
req.body = body
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
parsed = JSON.parse(response.body)
|
|
71
|
+
unless response.status >= 200 && response.status < 300
|
|
72
|
+
raise SeekmodoError, "MCP gateway returned HTTP #{response.status}: #{response.body}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if parsed["error"]
|
|
76
|
+
raise SeekmodoError, "MCP error #{parsed['error']}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
parsed["result"]
|
|
80
|
+
rescue JSON::ParserError => e
|
|
81
|
+
raise SeekmodoError, "MCP response was not valid JSON: #{e.message}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def build_headers(body)
|
|
85
|
+
headers = {
|
|
86
|
+
"Content-Type" => "application/json",
|
|
87
|
+
"Accept" => "application/json",
|
|
88
|
+
"User-Agent" => @user_agent
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if @operator_token
|
|
92
|
+
headers["Authorization"] = "Bearer #{@operator_token}"
|
|
93
|
+
if @tenant_id
|
|
94
|
+
headers[HmacSigner::HEADER_TENANT] = @tenant_id
|
|
95
|
+
end
|
|
96
|
+
elsif @signer
|
|
97
|
+
headers.merge!(@signer.headers(body))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
headers
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Seekmodo
|
|
4
|
+
module Sdk
|
|
5
|
+
module Mode
|
|
6
|
+
OFF = "off"
|
|
7
|
+
LEARNING = "learning"
|
|
8
|
+
SHADOW = "shadow"
|
|
9
|
+
ACTIVE = "active"
|
|
10
|
+
ENFORCE = "enforce"
|
|
11
|
+
|
|
12
|
+
ALL = [OFF, LEARNING, SHADOW, ACTIVE, ENFORCE].freeze
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def values
|
|
17
|
+
ALL
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def valid?(mode)
|
|
21
|
+
ALL.include?(mode)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def assert_mode(mode)
|
|
25
|
+
unless valid?(mode)
|
|
26
|
+
raise ArgumentError, "Unknown Seekmodo mode '#{mode}'; expected one of: #{ALL.join(', ')}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
mode
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def serves_search?(mode)
|
|
33
|
+
[SHADOW, ACTIVE, ENFORCE].include?(mode)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def mirrors_writes?(mode)
|
|
37
|
+
mode != OFF
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "mode"
|
|
4
|
+
require_relative "circuit_breaker"
|
|
5
|
+
require_relative "tenant_snapshot"
|
|
6
|
+
|
|
7
|
+
module Seekmodo
|
|
8
|
+
module Sdk
|
|
9
|
+
class ModeFsm
|
|
10
|
+
def initialize(snapshot, breaker: nil, default_mode: Mode::OFF)
|
|
11
|
+
@snapshot = snapshot
|
|
12
|
+
@breaker = breaker
|
|
13
|
+
@default_mode = Mode.assert_mode(default_mode)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def effective_mode
|
|
17
|
+
if @breaker && @breaker.state == CircuitBreaker::STATE_OPEN
|
|
18
|
+
return Mode::OFF
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
config = @snapshot.get
|
|
22
|
+
configured = config.fetch("mode", @default_mode).to_s
|
|
23
|
+
return @default_mode unless Mode.valid?(configured)
|
|
24
|
+
|
|
25
|
+
if configured != Mode::ACTIVE
|
|
26
|
+
return configured
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
fsm_state = config.dig("fsm", "current_state").to_s
|
|
30
|
+
if Mode.valid?(fsm_state) && fsm_state != Mode::ACTIVE
|
|
31
|
+
return fsm_state
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
Mode::SHADOW
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def configured_mode
|
|
38
|
+
config = @snapshot.get
|
|
39
|
+
configured = config.fetch("mode", @default_mode).to_s
|
|
40
|
+
Mode.valid?(configured) ? configured : @default_mode
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def serves_search?
|
|
44
|
+
Mode.serves_search?(effective_mode)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def mirrors_writes?
|
|
48
|
+
Mode.mirrors_writes?(effective_mode)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "json"
|
|
5
|
+
require "jwt"
|
|
6
|
+
require "openssl"
|
|
7
|
+
require "faraday"
|
|
8
|
+
|
|
9
|
+
require_relative "exceptions/seekmodo_error"
|
|
10
|
+
require_relative "storage/protocols"
|
|
11
|
+
|
|
12
|
+
module Seekmodo
|
|
13
|
+
module Sdk
|
|
14
|
+
class Pairing
|
|
15
|
+
DEFAULT_JWKS_URL = "https://seekmodo.com/.well-known/jwks.json"
|
|
16
|
+
KEYS_CACHE_KEY = "numinix.seekmodo.pairing.jwks"
|
|
17
|
+
KEYS_CACHE_TTL_SECONDS = 86400
|
|
18
|
+
MAX_TOKEN_AGE_SECONDS = 600
|
|
19
|
+
|
|
20
|
+
def initialize(connection, cache, jwks_url: DEFAULT_JWKS_URL, clock: nil)
|
|
21
|
+
@connection = connection
|
|
22
|
+
@cache = cache
|
|
23
|
+
@jwks_url = jwks_url
|
|
24
|
+
@clock = clock || -> { Time.now.to_i }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def verify_and_extract(jwt_token)
|
|
28
|
+
header, payload, _sig = JWT.decode(jwt_token, nil, false)
|
|
29
|
+
alg = header["alg"].to_s
|
|
30
|
+
raise SeekmodoError, "Unsupported pairing JWT alg \"#{alg}\"; expected EdDSA." unless alg == "EdDSA"
|
|
31
|
+
|
|
32
|
+
kid = header["kid"].to_s
|
|
33
|
+
raise SeekmodoError, "Pairing JWT is missing kid header." if kid.empty?
|
|
34
|
+
|
|
35
|
+
jwk = lookup_key(kid, refresh: false)
|
|
36
|
+
jwk = lookup_key(kid, refresh: true) if jwk.nil?
|
|
37
|
+
raise SeekmodoError, "No pairing JWKS key matches kid=\"#{kid}\"; rotation lag?" if jwk.nil?
|
|
38
|
+
|
|
39
|
+
public_key_raw = base64url_decode(jwk["x"].to_s)
|
|
40
|
+
raise SeekmodoError, "Pairing JWKS key has wrong byte length for Ed25519." unless public_key_raw.bytesize == 32
|
|
41
|
+
|
|
42
|
+
public_key = OpenSSL::PKey.read({
|
|
43
|
+
kty: "OKP",
|
|
44
|
+
crv: "Ed25519",
|
|
45
|
+
x: jwk["x"]
|
|
46
|
+
}.to_json)
|
|
47
|
+
|
|
48
|
+
JWT.decode(
|
|
49
|
+
jwt_token,
|
|
50
|
+
public_key,
|
|
51
|
+
true,
|
|
52
|
+
{ algorithm: "EdDSA" }
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
now = @clock.call
|
|
56
|
+
exp = payload["exp"].to_i
|
|
57
|
+
iat = payload["iat"].to_i
|
|
58
|
+
raise SeekmodoError, "Pairing JWT has expired." if exp > 0 && exp < now
|
|
59
|
+
raise SeekmodoError, "Pairing JWT is older than the 10-minute replay window." if iat > 0 && now - iat > MAX_TOKEN_AGE_SECONDS
|
|
60
|
+
|
|
61
|
+
payload
|
|
62
|
+
rescue JWT::DecodeError => e
|
|
63
|
+
raise SeekmodoError, "Pairing JWT signature verification failed.", e.backtrace
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def refresh_keys
|
|
67
|
+
fetch_keys
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def lookup_key(kid, refresh:)
|
|
73
|
+
keys_doc = refresh ? fetch_keys : cached_keys
|
|
74
|
+
keys_doc.fetch("keys", []).find { |key| key.is_a?(Hash) && key["kid"] == kid }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def cached_keys
|
|
78
|
+
cached = @cache.get(KEYS_CACHE_KEY)
|
|
79
|
+
return cached if cached.is_a?(Hash)
|
|
80
|
+
|
|
81
|
+
fetch_keys
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def fetch_keys
|
|
85
|
+
response = @connection.get(@jwks_url) do |req|
|
|
86
|
+
req.headers["Accept"] = "application/json"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
raise SeekmodoError, "JWKS fetch returned HTTP #{response.status}" unless response.status == 200
|
|
90
|
+
|
|
91
|
+
body = JSON.parse(response.body)
|
|
92
|
+
unless body.is_a?(Hash) && body["keys"].is_a?(Array) && body["keys"].any?
|
|
93
|
+
raise SeekmodoError, 'JWKS response did not contain a "keys" array.'
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
@cache.set(KEYS_CACHE_KEY, body, KEYS_CACHE_TTL_SECONDS)
|
|
97
|
+
body
|
|
98
|
+
rescue Faraday::Error => e
|
|
99
|
+
raise SeekmodoError, "Failed to fetch Seekmodo pairing JWKS: #{e.message}"
|
|
100
|
+
rescue JSON::ParserError => e
|
|
101
|
+
raise SeekmodoError, "JWKS response was not valid JSON: #{e.message}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def base64url_decode(segment)
|
|
105
|
+
padded = segment.tr("-_", "+/")
|
|
106
|
+
remainder = padded.length % 4
|
|
107
|
+
padded += "=" * (4 - remainder) if remainder.positive?
|
|
108
|
+
Base64.decode64(padded)
|
|
109
|
+
rescue ArgumentError => e
|
|
110
|
+
raise SeekmodoError, "Pairing JWT contains malformed base64url segment.", e.backtrace
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "storage/protocols"
|
|
4
|
+
|
|
5
|
+
module Seekmodo
|
|
6
|
+
module Sdk
|
|
7
|
+
class SignatureMismatchTracker
|
|
8
|
+
CACHE_KEY = "numinix.seekmodo.sigmismatch_failures"
|
|
9
|
+
DEFAULT_WINDOW_SECONDS = 300
|
|
10
|
+
DEFAULT_THRESHOLD = 3
|
|
11
|
+
|
|
12
|
+
def initialize(cache, window_seconds: DEFAULT_WINDOW_SECONDS, threshold: DEFAULT_THRESHOLD, clock: nil)
|
|
13
|
+
@cache = cache
|
|
14
|
+
@window_seconds = window_seconds
|
|
15
|
+
@threshold = threshold
|
|
16
|
+
@clock = clock || -> { Time.now.to_i }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def record_failure
|
|
20
|
+
now = @clock.call
|
|
21
|
+
rows = load_and_prune(now)
|
|
22
|
+
rows << now
|
|
23
|
+
@cache.set(CACHE_KEY, rows, [@window_seconds * 2, 60].max)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def clear
|
|
27
|
+
@cache.delete(CACHE_KEY)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def tripped?
|
|
31
|
+
load_and_prune(@clock.call).length >= @threshold
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def failures_in_window
|
|
35
|
+
load_and_prune(@clock.call).length
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def load_and_prune(now)
|
|
41
|
+
cutoff = now - @window_seconds
|
|
42
|
+
stored = @cache.get(CACHE_KEY, [])
|
|
43
|
+
stored = [] unless stored.is_a?(Array)
|
|
44
|
+
pruned = stored.select { |ts| ts.is_a?(Numeric) && ts.to_i >= cutoff }.map(&:to_i)
|
|
45
|
+
if pruned.length != stored.length
|
|
46
|
+
@cache.set(CACHE_KEY, pruned, [@window_seconds * 2, 60].max)
|
|
47
|
+
end
|
|
48
|
+
pruned
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../protocols"
|
|
4
|
+
|
|
5
|
+
module Seekmodo
|
|
6
|
+
module Sdk
|
|
7
|
+
module Storage
|
|
8
|
+
module Memory
|
|
9
|
+
DEFAULT_BREAKER_STATE = {
|
|
10
|
+
"state" => "closed",
|
|
11
|
+
"failures" => [],
|
|
12
|
+
"opened_at" => 0,
|
|
13
|
+
"probe_in_flight" => false
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
class BreakerStore
|
|
17
|
+
include Protocols::BreakerStateStore
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
@rows = {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def load(key)
|
|
24
|
+
state = @rows[key]
|
|
25
|
+
return DEFAULT_BREAKER_STATE.dup unless state
|
|
26
|
+
|
|
27
|
+
{
|
|
28
|
+
"state" => state["state"],
|
|
29
|
+
"failures" => state["failures"].dup,
|
|
30
|
+
"opened_at" => state["opened_at"].to_i,
|
|
31
|
+
"probe_in_flight" => state["probe_in_flight"]
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def save(key, state, ttl_seconds)
|
|
36
|
+
@rows[key] = {
|
|
37
|
+
"state" => state["state"],
|
|
38
|
+
"failures" => state["failures"].dup,
|
|
39
|
+
"opened_at" => state["opened_at"].to_i,
|
|
40
|
+
"probe_in_flight" => state["probe_in_flight"]
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class Cache
|
|
46
|
+
include Protocols::Cache
|
|
47
|
+
|
|
48
|
+
Entry = Struct.new(:value, :expires_at, keyword_init: true)
|
|
49
|
+
|
|
50
|
+
def initialize(clock = nil)
|
|
51
|
+
@rows = {}
|
|
52
|
+
@clock = clock || -> { Time.now.to_f }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def get(key, default = nil)
|
|
56
|
+
entry = @rows[key]
|
|
57
|
+
return default unless entry
|
|
58
|
+
|
|
59
|
+
if entry.expires_at < @clock.call
|
|
60
|
+
@rows.delete(key)
|
|
61
|
+
return default
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
entry.value
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def set(key, value, ttl_seconds)
|
|
68
|
+
@rows[key] = Entry.new(value: value, expires_at: @clock.call + ttl_seconds)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def delete(key)
|
|
72
|
+
@rows.delete(key)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
class EventQueueStore
|
|
77
|
+
include Protocols::EventQueueStore
|
|
78
|
+
|
|
79
|
+
def initialize
|
|
80
|
+
@events = []
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def push(event)
|
|
84
|
+
@events << event.dup
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def drain(max_events)
|
|
88
|
+
batch = @events.take(max_events)
|
|
89
|
+
@events = @events.drop(max_events)
|
|
90
|
+
batch
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def count
|
|
94
|
+
@events.length
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Seekmodo
|
|
4
|
+
module Sdk
|
|
5
|
+
module Storage
|
|
6
|
+
module Protocols
|
|
7
|
+
module Cache
|
|
8
|
+
def get(key, default = nil)
|
|
9
|
+
raise NotImplementedError
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def set(key, value, ttl_seconds)
|
|
13
|
+
raise NotImplementedError
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def delete(key)
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
module BreakerStateStore
|
|
22
|
+
def load(key)
|
|
23
|
+
raise NotImplementedError
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def save(key, state, ttl_seconds)
|
|
27
|
+
raise NotImplementedError
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
module EventQueueStore
|
|
32
|
+
def push(event)
|
|
33
|
+
raise NotImplementedError
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def drain(max_events)
|
|
37
|
+
raise NotImplementedError
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def count
|
|
41
|
+
raise NotImplementedError
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "transport"
|
|
4
|
+
|
|
5
|
+
module Seekmodo
|
|
6
|
+
module Sdk
|
|
7
|
+
module Storefront
|
|
8
|
+
class Client
|
|
9
|
+
attr_reader :transport, :recommend, :bundle
|
|
10
|
+
|
|
11
|
+
def initialize(config)
|
|
12
|
+
@transport = Transport.new(**config)
|
|
13
|
+
@recommend = RecommendSurface.new(@transport)
|
|
14
|
+
@bundle = BundleSurface.new(@transport)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def search(args = {}, opts = {})
|
|
18
|
+
@transport.call("search", args, opts)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def suggest(args = {}, opts = {})
|
|
22
|
+
@transport.call("suggest", args, opts)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def search_by_image(args = {}, opts = {})
|
|
26
|
+
@transport.call("search.byImage", args, opts)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def chat(args = {}, opts = {})
|
|
30
|
+
@transport.call("chat", args, opts)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def event(args = {}, opts = {})
|
|
34
|
+
@transport.call("events", args, opts)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class RecommendSurface
|
|
38
|
+
def initialize(transport)
|
|
39
|
+
@transport = transport
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def related(args = {}, opts = {})
|
|
43
|
+
@transport.call("recommend.related", args, opts)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def also_bought(args = {}, opts = {})
|
|
47
|
+
@transport.call("recommend.also_bought", args, opts)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def also_viewed(args = {}, opts = {})
|
|
51
|
+
@transport.call("recommend.also_viewed", args, opts)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def trending(args = {}, opts = {})
|
|
55
|
+
@transport.call("recommend.trending", args, opts)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class BundleSurface
|
|
60
|
+
def initialize(transport)
|
|
61
|
+
@transport = transport
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def suggest(args = {}, opts = {})
|
|
65
|
+
@transport.call("bundle.suggest", args, opts)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|