smplkit 1.0.5

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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE +21 -0
  4. data/README.md +105 -0
  5. data/lib/smplkit/client.rb +218 -0
  6. data/lib/smplkit/config/client.rb +238 -0
  7. data/lib/smplkit/config/helpers.rb +108 -0
  8. data/lib/smplkit/config/models.rb +192 -0
  9. data/lib/smplkit/config_resolution.rb +202 -0
  10. data/lib/smplkit/context.rb +68 -0
  11. data/lib/smplkit/debug.rb +50 -0
  12. data/lib/smplkit/errors.rb +114 -0
  13. data/lib/smplkit/flags/client.rb +480 -0
  14. data/lib/smplkit/flags/helpers.rb +76 -0
  15. data/lib/smplkit/flags/models.rb +258 -0
  16. data/lib/smplkit/flags/types.rb +233 -0
  17. data/lib/smplkit/generators/install_generator.rb +42 -0
  18. data/lib/smplkit/helpers.rb +15 -0
  19. data/lib/smplkit/log_level.rb +57 -0
  20. data/lib/smplkit/logging/adapters/base.rb +63 -0
  21. data/lib/smplkit/logging/adapters/semantic_logger_adapter.rb +88 -0
  22. data/lib/smplkit/logging/adapters/stdlib_logger_adapter.rb +143 -0
  23. data/lib/smplkit/logging/client.rb +142 -0
  24. data/lib/smplkit/logging/helpers.rb +69 -0
  25. data/lib/smplkit/logging/levels.rb +86 -0
  26. data/lib/smplkit/logging/models.rb +124 -0
  27. data/lib/smplkit/logging/normalize.rb +16 -0
  28. data/lib/smplkit/logging/sources.rb +44 -0
  29. data/lib/smplkit/management/buffer.rb +111 -0
  30. data/lib/smplkit/management/client.rb +623 -0
  31. data/lib/smplkit/management/models.rb +133 -0
  32. data/lib/smplkit/management/types.rb +65 -0
  33. data/lib/smplkit/metrics.rb +78 -0
  34. data/lib/smplkit/railtie.rb +48 -0
  35. data/lib/smplkit/version.rb +5 -0
  36. data/lib/smplkit/ws.rb +92 -0
  37. data/lib/smplkit.rb +43 -0
  38. data/sig/smplkit.rbs +141 -0
  39. metadata +139 -0
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smplkit
4
+ module Management
5
+ # An environment resource — a customer-defined deploy target (production,
6
+ # staging, etc.) for which configs and flags can have overrides.
7
+ class Environment
8
+ attr_accessor :id, :key, :name, :color, :classification, :description, :created_at, :updated_at
9
+
10
+ def initialize(client = nil, key:, id: nil, name: nil, color: nil,
11
+ classification: EnvironmentClassification::STANDARD,
12
+ description: nil, created_at: nil, updated_at: nil)
13
+ @client = client
14
+ @id = id
15
+ @key = key
16
+ @name = name
17
+ @color = color
18
+ @classification = classification
19
+ @description = description
20
+ @created_at = created_at
21
+ @updated_at = updated_at
22
+ end
23
+
24
+ def save
25
+ raise "Environment was constructed without a client; cannot save" if @client.nil?
26
+
27
+ updated =
28
+ if @created_at.nil?
29
+ @client._create_environment(self)
30
+ else
31
+ @client._update_environment(self)
32
+ end
33
+ _apply(updated)
34
+ self
35
+ end
36
+ alias save! save
37
+
38
+ def delete
39
+ raise "Environment was constructed without a client; cannot delete" if @client.nil?
40
+
41
+ @client.delete(@key)
42
+ end
43
+ alias delete! delete
44
+
45
+ def _apply(other)
46
+ @id = other.id
47
+ @key = other.key
48
+ @name = other.name
49
+ @color = other.color
50
+ @classification = other.classification
51
+ @description = other.description
52
+ @created_at = other.created_at
53
+ @updated_at = other.updated_at
54
+ end
55
+ end
56
+
57
+ # A context type resource (e.g. "user", "account").
58
+ class ContextType
59
+ attr_accessor :id, :key, :name, :description, :created_at, :updated_at
60
+
61
+ def initialize(client = nil, key:, id: nil, name: nil, description: nil,
62
+ created_at: nil, updated_at: nil)
63
+ @client = client
64
+ @id = id
65
+ @key = key
66
+ @name = name
67
+ @description = description
68
+ @created_at = created_at
69
+ @updated_at = updated_at
70
+ end
71
+
72
+ def save
73
+ raise "ContextType was constructed without a client; cannot save" if @client.nil?
74
+
75
+ updated =
76
+ if @created_at.nil?
77
+ @client._create_context_type(self)
78
+ else
79
+ @client._update_context_type(self)
80
+ end
81
+ _apply(updated)
82
+ self
83
+ end
84
+ alias save! save
85
+
86
+ def delete
87
+ raise "ContextType was constructed without a client; cannot delete" if @client.nil?
88
+
89
+ @client.delete(@key)
90
+ end
91
+ alias delete! delete
92
+
93
+ def _apply(other)
94
+ @id = other.id
95
+ @key = other.key
96
+ @name = other.name
97
+ @description = other.description
98
+ @created_at = other.created_at
99
+ @updated_at = other.updated_at
100
+ end
101
+ end
102
+
103
+ # An account-wide settings resource.
104
+ class AccountSettings
105
+ attr_accessor :id, :environment_order, :default_environment, :updated_at
106
+
107
+ def initialize(client = nil, id: nil, environment_order: nil,
108
+ default_environment: nil, updated_at: nil)
109
+ @client = client
110
+ @id = id
111
+ @environment_order = environment_order || []
112
+ @default_environment = default_environment
113
+ @updated_at = updated_at
114
+ end
115
+
116
+ def save
117
+ raise "AccountSettings was constructed without a client; cannot save" if @client.nil?
118
+
119
+ updated = @client._update_account_settings(self)
120
+ _apply(updated)
121
+ self
122
+ end
123
+ alias save! save
124
+
125
+ def _apply(other)
126
+ @id = other.id
127
+ @environment_order = other.environment_order
128
+ @default_environment = other.default_environment
129
+ @updated_at = other.updated_at
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smplkit
4
+ module Management
5
+ # Whether an environment participates in the canonical ordering.
6
+ #
7
+ # +STANDARD+ environments are the customer's deploy targets — production,
8
+ # staging, development, etc.
9
+ # +AD_HOC+ environments are transient targets (preview branches, individual
10
+ # developer sandboxes) that should not appear in the standard ordering.
11
+ module EnvironmentClassification
12
+ STANDARD = "STANDARD"
13
+ AD_HOC = "AD_HOC"
14
+
15
+ ALL = [STANDARD, AD_HOC].freeze
16
+ end
17
+
18
+ HEX_RE = /\A#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\z/
19
+
20
+ # A color, expressed as a CSS hex string.
21
+ #
22
+ # Smplkit::Color.new("#ef4444") # 6-digit hex
23
+ # Smplkit::Color.new("#fff") # 3-digit shorthand
24
+ # Smplkit::Color.new("#ef4444aa") # 8-digit with alpha
25
+ # Smplkit::Color.rgb(239, 68, 68) # RGB components
26
+ #
27
+ # Frozen — construct a fresh +Color+ to change a value.
28
+ class Color
29
+ attr_reader :hex
30
+
31
+ def initialize(hex)
32
+ raise TypeError, "Color hex must be a String, got #{hex.class}: #{hex.inspect}" unless hex.is_a?(String)
33
+ unless HEX_RE.match?(hex)
34
+ raise ArgumentError,
35
+ "Invalid color #{hex.inspect}: must be a CSS hex string like '#RGB', '#RRGGBB', or '#RRGGBBAA'"
36
+ end
37
+
38
+ @hex = hex.downcase.freeze
39
+ freeze
40
+ end
41
+
42
+ def self.rgb(red, green, blue)
43
+ [%w[red green blue], [red, green, blue]].transpose.each do |name, val|
44
+ unless val.is_a?(Integer) && !val.is_a?(TrueClass) && !val.is_a?(FalseClass)
45
+ raise TypeError,
46
+ "Color.rgb #{name} must be an Integer, got #{val.class}"
47
+ end
48
+ raise ArgumentError, "Color.rgb #{name} must be in range 0-255, got #{val.inspect}" unless val.between?(0,
49
+ 255)
50
+ end
51
+
52
+ new("##{red.to_s(16).rjust(2, "0")}#{green.to_s(16).rjust(2, "0")}#{blue.to_s(16).rjust(2, "0")}")
53
+ end
54
+
55
+ def to_s = @hex
56
+ def to_str = @hex
57
+ def ==(other) = other.is_a?(Color) && other.hex == @hex
58
+ alias eql? ==
59
+ def hash = @hex.hash
60
+ end
61
+ end
62
+
63
+ Color = Management::Color
64
+ EnvironmentClassification = Management::EnvironmentClassification
65
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+
5
+ module Smplkit
6
+ # Periodically flushes accumulated SDK telemetry to the platform.
7
+ #
8
+ # Aggregation runs on a daemon thread. Public methods are thread-safe and
9
+ # block briefly only to enqueue metrics.
10
+ #
11
+ # When +telemetry+ is disabled in resolved config, +Smplkit::Client+ does
12
+ # not construct a +MetricsReporter+ and the +metrics+ accessor on
13
+ # sub-clients is +nil+. Sub-clients guard every call with +metrics&.+.
14
+ class MetricsReporter
15
+ DEFAULT_FLUSH_INTERVAL = 60.0
16
+
17
+ def initialize(http_client:, environment:, service:, flush_interval: DEFAULT_FLUSH_INTERVAL)
18
+ @http_client = http_client
19
+ @environment = environment
20
+ @service = service
21
+ @flush_interval = flush_interval
22
+ @counts = {}
23
+ @gauges = {}
24
+ @closed = Concurrent::AtomicBoolean.new(false)
25
+ @lock = Mutex.new
26
+ schedule_flush
27
+ end
28
+
29
+ def record(metric, unit:, dimensions: nil)
30
+ return if @closed.true?
31
+
32
+ key = [metric, unit, dimensions || {}].freeze
33
+ @lock.synchronize { @counts[key] = (@counts[key] || 0) + 1 }
34
+ end
35
+
36
+ def record_gauge(metric, value, unit:, dimensions: nil)
37
+ return if @closed.true?
38
+
39
+ key = [metric, unit, dimensions || {}].freeze
40
+ @lock.synchronize { @gauges[key] = value }
41
+ end
42
+
43
+ def flush
44
+ counts_snap, gauges_snap = @lock.synchronize do
45
+ c = @counts.dup
46
+ g = @gauges.dup
47
+ @counts.clear
48
+ @gauges.clear
49
+ [c, g]
50
+ end
51
+ send_payload(counts_snap, gauges_snap) unless counts_snap.empty? && gauges_snap.empty?
52
+ rescue StandardError => e
53
+ Smplkit.debug("metrics", "flush failed: #{e.class}: #{e.message}")
54
+ end
55
+
56
+ def close
57
+ return if @closed.true?
58
+
59
+ @closed.make_true
60
+ @timer&.shutdown
61
+ flush
62
+ end
63
+
64
+ private
65
+
66
+ def schedule_flush
67
+ @timer = Concurrent::TimerTask.new(execution_interval: @flush_interval) { flush }
68
+ @timer.execute
69
+ end
70
+
71
+ def send_payload(_counts, _gauges)
72
+ # Telemetry payload submission is a no-op stub in the initial Ruby SDK
73
+ # release. The Python SDK posts to /api/metrics/v1; this hook is here so
74
+ # the same wiring can be filled in once the generated client surface is
75
+ # finalized.
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smplkit
4
+ # Rails integration for the smplkit gem.
5
+ #
6
+ # The Railtie is loaded only when +Rails::Railtie+ is defined, so non-Rails
7
+ # customers pay zero load cost.
8
+ #
9
+ # Customers configure smplkit through +config.smplkit.*+ in
10
+ # +config/initializers/smplkit.rb+, generated by
11
+ # +rails generate smplkit:install+.
12
+ class Railtie < ::Rails::Railtie
13
+ config.smplkit = ActiveSupport::OrderedOptions.new
14
+
15
+ initializer "smplkit.configure" do |app|
16
+ Smplkit.configure_for_rails(app.config.smplkit)
17
+ end
18
+
19
+ generators do
20
+ require_relative "generators/install_generator"
21
+ end
22
+ end
23
+
24
+ module_function
25
+
26
+ # Construct the global +Smplkit.client+ from a Rails-friendly options
27
+ # struct. Falls back to +SMPLKIT_*+ env vars + +~/.smplkit+ resolution
28
+ # for any field the customer left blank.
29
+ def configure_for_rails(options)
30
+ client = Client.new(
31
+ api_key: options.api_key,
32
+ environment: options.environment || (defined?(::Rails) ? ::Rails.env : nil),
33
+ service: options.service,
34
+ profile: options.profile,
35
+ base_domain: options.base_domain,
36
+ scheme: options.scheme,
37
+ debug: options.debug,
38
+ telemetry: options.telemetry
39
+ )
40
+ @client = client
41
+ @context_provider = options.context_provider
42
+ client
43
+ end
44
+
45
+ class << self
46
+ attr_accessor :client, :context_provider
47
+ end
48
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smplkit
4
+ VERSION = "0.0.0"
5
+ end
data/lib/smplkit/ws.rb ADDED
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+
5
+ module Smplkit
6
+ # Manages a single WebSocket connection to the app service event gateway.
7
+ #
8
+ # A single +SharedWebSocket+ instance is shared across all product modules
9
+ # (config, flags) within one +Smplkit::Client+. Product modules register
10
+ # listeners for specific event types; the shared connection dispatches
11
+ # incoming events to the appropriate listeners.
12
+ #
13
+ # The connection runs on a dedicated SDK-owned thread; public methods are
14
+ # thread-safe and non-blocking.
15
+ #
16
+ # The app service gateway protocol:
17
+ # - Connect to +wss://app.<base_domain>/api/ws/v1/events?api_key={key}+
18
+ # - Receive +{"type": "connected"}+ on success
19
+ # - Receive events: +{"event": "config_changed", ...}+, etc.
20
+ # - No subscribe message - the API key determines the account
21
+ # - Heartbeat: server sends +"ping"+ (text), client responds with +"pong"+
22
+ #
23
+ # NOTE: The actual WebSocket I/O is wired to async-websocket on a worker
24
+ # thread. The initial Ruby SDK release defers full live-update wiring to a
25
+ # follow-up because async-websocket interactions need integration testing
26
+ # against the real platform.
27
+ class SharedWebSocket
28
+ BACKOFF_SCHEDULE = [1, 2, 4, 8, 16, 32, 60].freeze
29
+
30
+ def initialize(app_base_url:, api_key:, metrics: nil)
31
+ @app_base_url = app_base_url
32
+ @api_key = api_key
33
+ @metrics = metrics
34
+ @listeners = Concurrent::Hash.new { |h, k| h[k] = [] }
35
+ @listeners_lock = Mutex.new
36
+ @connection_status = "disconnected"
37
+ @closed = false
38
+ end
39
+
40
+ def on(event_name, &callback)
41
+ @listeners_lock.synchronize { @listeners[event_name] << callback }
42
+ end
43
+
44
+ def off(event_name, callback)
45
+ @listeners_lock.synchronize { @listeners[event_name].delete(callback) }
46
+ end
47
+
48
+ def dispatch(event_name, data)
49
+ callbacks = @listeners_lock.synchronize { @listeners[event_name].dup }
50
+ callbacks.each do |cb|
51
+ cb.call(data)
52
+ rescue StandardError => e
53
+ Smplkit.debug("websocket", "listener for #{event_name} raised: #{e.class}: #{e.message}")
54
+ end
55
+ end
56
+
57
+ attr_reader :connection_status
58
+
59
+ # Marked as connected for in-process testing without a real WS connection.
60
+ # Production wiring overrides this from the I/O thread once the gateway
61
+ # confirms the handshake.
62
+ def mark_connected!
63
+ @connection_status = "connected"
64
+ end
65
+
66
+ def start
67
+ Smplkit.debug("websocket", "starting shared WebSocket (Ruby SDK initial release: in-memory only)")
68
+ # Live wiring is deferred. Behave as if the handshake succeeded so the
69
+ # rest of the runtime can proceed - listeners still fire for any events
70
+ # other code dispatches into this instance.
71
+ mark_connected!
72
+ end
73
+
74
+ def stop
75
+ @closed = true
76
+ @connection_status = "disconnected"
77
+ end
78
+
79
+ def build_ws_url
80
+ url = @app_base_url.dup
81
+ ws_url = if url.start_with?("https://")
82
+ "wss://#{url[("https://".length)..]}"
83
+ elsif url.start_with?("http://")
84
+ "ws://#{url[("http://".length)..]}"
85
+ else
86
+ "wss://#{url}"
87
+ end
88
+ ws_url = ws_url.chomp("/")
89
+ "#{ws_url}/api/ws/v1/events?api_key=#{@api_key}"
90
+ end
91
+ end
92
+ end
data/lib/smplkit.rb ADDED
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Official Ruby SDK for the smplkit platform.
4
+ #
5
+ # require "smplkit"
6
+ #
7
+ # client = Smplkit::Client.new(environment: "production", service: "my-svc")
8
+ # flag = client.flags.boolean_flag("checkout-v2", default: false)
9
+ # flag.get
10
+ module Smplkit
11
+ end
12
+
13
+ require_relative "smplkit/version"
14
+ require_relative "smplkit/errors"
15
+ require_relative "smplkit/debug"
16
+ require_relative "smplkit/helpers"
17
+ require_relative "smplkit/log_level"
18
+ require_relative "smplkit/context"
19
+ require_relative "smplkit/config_resolution"
20
+ require_relative "smplkit/metrics"
21
+ require_relative "smplkit/ws"
22
+ require_relative "smplkit/flags/types"
23
+ require_relative "smplkit/flags/models"
24
+ require_relative "smplkit/flags/helpers"
25
+ require_relative "smplkit/flags/client"
26
+ require_relative "smplkit/config/models"
27
+ require_relative "smplkit/config/helpers"
28
+ require_relative "smplkit/config/client"
29
+ require_relative "smplkit/logging/levels"
30
+ require_relative "smplkit/logging/normalize"
31
+ require_relative "smplkit/logging/sources"
32
+ require_relative "smplkit/logging/models"
33
+ require_relative "smplkit/logging/helpers"
34
+ require_relative "smplkit/logging/adapters/base"
35
+ require_relative "smplkit/logging/adapters/stdlib_logger_adapter"
36
+ require_relative "smplkit/logging/client"
37
+ require_relative "smplkit/management/types"
38
+ require_relative "smplkit/management/models"
39
+ require_relative "smplkit/management/buffer"
40
+ require_relative "smplkit/management/client"
41
+ require_relative "smplkit/client"
42
+
43
+ require_relative "smplkit/railtie" if defined?(Rails::Railtie)
data/sig/smplkit.rbs ADDED
@@ -0,0 +1,141 @@
1
+ # Top-level RBS signatures for the smplkit Ruby SDK.
2
+ #
3
+ # Per ADR-046 §2.8, sigs ship alongside the gem in +sig/+ for IDE/LSP
4
+ # autocompletion and Steep type-checking. Per-class sigs that go beyond
5
+ # the public surface declared here are deferred — see ISSUES.md.
6
+
7
+ module Smplkit
8
+ VERSION: String
9
+
10
+ def self.debug: (String, String) -> void
11
+ def self.enable_debug: () -> void
12
+ def self.set_request_context: (Array[Context]) -> ContextScope
13
+ def self.request_context: () -> Array[Context]
14
+ def self.configure_for_rails: (untyped) -> Client
15
+
16
+ class Client
17
+ def self.open: (**untyped) { (Client) -> void } -> void
18
+ def initialize: (?api_key: String?, ?environment: String?, ?service: String?,
19
+ ?profile: String?, ?base_domain: String?, ?scheme: String?,
20
+ ?debug: bool?, ?telemetry: bool?) -> void
21
+ def manage: () -> ManagementClient
22
+ def config: () -> Config::ConfigClient
23
+ def flags: () -> Flags::FlagsClient
24
+ def logging: () -> Logging::LoggingClient
25
+ def wait_until_ready: (?timeout: Float) -> void
26
+ def set_context: (Array[Context]) ?{ (ContextScope) -> void } -> ContextScope?
27
+ def close: () -> void
28
+ end
29
+
30
+ class ManagementClient
31
+ def initialize: (?api_key: String?, ?base_domain: String?, ?scheme: String?,
32
+ ?profile: String?, ?debug: bool?) -> void
33
+ def contexts: () -> ManagementClient::ContextsNamespace
34
+ def context_types: () -> ManagementClient::ContextTypesNamespace
35
+ def environments: () -> ManagementClient::EnvironmentsNamespace
36
+ def account_settings: () -> ManagementClient::AccountSettingsNamespace
37
+ def config: () -> ManagementClient::ConfigNamespace
38
+ def flags: () -> ManagementClient::FlagsNamespace
39
+ def loggers: () -> ManagementClient::LoggersNamespace
40
+ def log_groups: () -> ManagementClient::LogGroupsNamespace
41
+ def close: () -> void
42
+ end
43
+
44
+ class Context
45
+ attr_reader type: String
46
+ attr_reader key: String
47
+ attr_reader attributes: Hash[String, untyped]
48
+ attr_accessor name: String?
49
+ def initialize: (String, String, ?Hash[String | Symbol, untyped]?,
50
+ ?name: String?, **untyped) -> void
51
+ def id: () -> String
52
+ def save: () -> Context
53
+ alias save! save
54
+ def delete: () -> void
55
+ alias delete! delete
56
+ end
57
+
58
+ class ContextScope
59
+ def call: () { (ContextScope) -> void } -> void
60
+ def exit: () -> void
61
+ end
62
+
63
+ module Op
64
+ EQ: String
65
+ NEQ: String
66
+ LT: String
67
+ LTE: String
68
+ GT: String
69
+ GTE: String
70
+ IN: String
71
+ CONTAINS: String
72
+ ALL: Array[String]
73
+ end
74
+
75
+ class Rule
76
+ def initialize: (String, environment: String) -> void
77
+ def when: (*untyped) -> Rule
78
+ def serve: (untyped) -> Hash[String, untyped]
79
+ end
80
+
81
+ class FlagDeclaration
82
+ attr_reader id: String
83
+ attr_reader type: String
84
+ attr_reader default: untyped
85
+ attr_reader service: String?
86
+ attr_reader environment: String?
87
+ def initialize: (id: String, type: String, default: untyped,
88
+ ?service: String?, ?environment: String?) -> void
89
+ end
90
+
91
+ class LogLevel
92
+ include Comparable
93
+ attr_reader name: String
94
+ attr_reader ordinal: Integer
95
+
96
+ TRACE: LogLevel
97
+ DEBUG: LogLevel
98
+ INFO: LogLevel
99
+ WARN: LogLevel
100
+ ERROR: LogLevel
101
+ FATAL: LogLevel
102
+ SILENT: LogLevel
103
+ ALL: Array[LogLevel]
104
+ NAMES: Array[String]
105
+
106
+ def self.from_string: (String | Symbol) -> LogLevel
107
+ def self.coerce: (LogLevel | String) -> LogLevel
108
+ end
109
+
110
+ class Error < StandardError
111
+ attr_reader errors: Array[ApiErrorDetail]
112
+ attr_reader status_code: Integer?
113
+ def initialize: (?String?, ?errors: Array[ApiErrorDetail]?, ?status_code: Integer?) -> void
114
+ end
115
+
116
+ class ConnectionError < Error
117
+ end
118
+
119
+ class TimeoutError < Error
120
+ end
121
+
122
+ class NotFoundError < Error
123
+ end
124
+
125
+ class ConflictError < Error
126
+ end
127
+
128
+ class ValidationError < Error
129
+ end
130
+
131
+ class ApiErrorDetail
132
+ attr_reader status: String?
133
+ attr_reader title: String?
134
+ attr_reader detail: String?
135
+ attr_reader source: Hash[untyped, untyped]
136
+ def initialize: (?status: String?, ?title: String?, ?detail: String?,
137
+ ?source: Hash[untyped, untyped]?) -> void
138
+ def to_h: () -> Hash[String, untyped]
139
+ def to_json: (*untyped) -> String
140
+ end
141
+ end