smplkit 3.0.95 → 3.0.97

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/lib/smplkit/account/client.rb +128 -0
  3. data/lib/smplkit/account/models.rb +71 -0
  4. data/lib/smplkit/api_support.rb +91 -0
  5. data/lib/smplkit/audit/buffer.rb +3 -1
  6. data/lib/smplkit/audit/categories.rb +21 -10
  7. data/lib/smplkit/audit/client.rb +18 -9
  8. data/lib/smplkit/audit/event_types.rb +26 -10
  9. data/lib/smplkit/audit/events.rb +93 -17
  10. data/lib/smplkit/{management/audit.rb → audit/forwarders.rb} +93 -85
  11. data/lib/smplkit/audit/models.rb +86 -32
  12. data/lib/smplkit/audit/resource_types.rb +21 -9
  13. data/lib/smplkit/buffers.rb +250 -0
  14. data/lib/smplkit/client.rb +161 -70
  15. data/lib/smplkit/config/client.rb +874 -186
  16. data/lib/smplkit/config/helpers.rb +44 -6
  17. data/lib/smplkit/config/models.rb +114 -7
  18. data/lib/smplkit/config_resolution.rb +17 -9
  19. data/lib/smplkit/errors.rb +14 -3
  20. data/lib/smplkit/flags/client.rb +602 -116
  21. data/lib/smplkit/flags/models.rb +110 -8
  22. data/lib/smplkit/flags/types.rb +8 -9
  23. data/lib/smplkit/jobs/client.rb +306 -0
  24. data/lib/smplkit/jobs/models.rb +47 -18
  25. data/lib/smplkit/logging/client.rb +755 -191
  26. data/lib/smplkit/logging/helpers.rb +5 -1
  27. data/lib/smplkit/logging/levels.rb +3 -1
  28. data/lib/smplkit/logging/models.rb +163 -6
  29. data/lib/smplkit/logging/normalize.rb +3 -1
  30. data/lib/smplkit/logging/resolution.rb +4 -4
  31. data/lib/smplkit/logging/sources.rb +1 -1
  32. data/lib/smplkit/platform/client.rb +597 -0
  33. data/lib/smplkit/platform/models.rb +282 -0
  34. data/lib/smplkit/{management → platform}/types.rb +21 -4
  35. data/lib/smplkit/transport.rb +103 -0
  36. data/lib/smplkit/ws.rb +1 -1
  37. data/lib/smplkit.rb +18 -6
  38. metadata +11 -7
  39. data/lib/smplkit/management/buffer.rb +0 -198
  40. data/lib/smplkit/management/client.rb +0 -1074
  41. data/lib/smplkit/management/jobs.rb +0 -226
  42. data/lib/smplkit/management/models.rb +0 -178
@@ -2,10 +2,19 @@
2
2
 
3
3
  module Smplkit
4
4
  module Config
5
+ # Internal conversion and resolution helpers shared by the config client.
6
+ #
7
+ # @api private
5
8
  module Helpers
6
9
  module_function
7
10
 
8
11
  # Translate a JSON:API resource Hash into a Config domain model.
12
+ #
13
+ # @api private
14
+ # @param client [ConfigClient, nil] The owning client, or +nil+ for a
15
+ # detached model.
16
+ # @param resource [Hash{String => Object}] The JSON:API resource Hash.
17
+ # @return [Config] The constructed config domain model.
9
18
  def config_from_json(client, resource)
10
19
  attrs = resource["attributes"] || {}
11
20
  items = (attrs["items"] || {}).map do |name, item|
@@ -22,8 +31,8 @@ module Smplkit
22
31
  end
23
32
 
24
33
  environments = (attrs["environments"] || {}).each_with_object({}) do |(env, env_data), out|
25
- # Per ADR-024 §2.4 env_data is already the flat override map
26
- # +{key: rawValue}+ — the old +{values: {...}}+ envelope is gone.
34
+ # env_data is already the flat override map +{key: rawValue}+ — the
35
+ # old +{values: {...}}+ envelope is gone.
27
36
  env_values = env_data.is_a?(Hash) ? env_data : {}
28
37
  out[env] = ConfigEnvironment.new(values: env_values)
29
38
  end
@@ -44,6 +53,12 @@ module Smplkit
44
53
 
45
54
  # Deep-merge two Hashes, with +override+ winning. Mirrors the Python
46
55
  # +deep_merge+ helper used by the resolver.
56
+ #
57
+ # @api private
58
+ # @param base [Hash{String => Object}] The base Hash.
59
+ # @param override [Hash{String => Object}] The Hash whose values win on
60
+ # conflict.
61
+ # @return [Hash{String => Object}] A new merged Hash.
47
62
  def deep_merge(base, override)
48
63
  result = base.dup
49
64
  override.each do |key, value|
@@ -57,6 +72,10 @@ module Smplkit
57
72
  end
58
73
 
59
74
  # Unwrap typed items +{ key => { value, type, desc } }+ to +{ key => raw }+.
75
+ #
76
+ # @api private
77
+ # @param items [Hash{String => Object}] Typed items keyed by item key.
78
+ # @return [Hash{String => Object}] The items as +{ key => raw_value }+.
60
79
  def unwrap_items(items)
61
80
  items.each_with_object({}) do |(k, v), out|
62
81
  out[k] = v.is_a?(Hash) && v.key?("value") ? v["value"] : v
@@ -66,6 +85,13 @@ module Smplkit
66
85
  # Build the parent chain (child-first, root-last) for a +Config+,
67
86
  # walking +parent_id+ pointers across the +by_id+ map. Mirrors the
68
87
  # Python SDK's client-side chain construction.
88
+ #
89
+ # @api private
90
+ # @param target [Config] The config to build the chain for.
91
+ # @param by_id [Hash{String => Config}] Pre-fetched configs keyed by id,
92
+ # used to look up parents without extra network calls.
93
+ # @return [Array<Hash{String => Object}>] Chain entries from child to
94
+ # root.
69
95
  def build_chain(target, by_id)
70
96
  chain = []
71
97
  current = target
@@ -84,14 +110,19 @@ module Smplkit
84
110
 
85
111
  # Build a single chain entry (the +id+/+items+/+environments+ Hash
86
112
  # shape used by +resolve_chain+) from a +Config+ domain model.
113
+ #
114
+ # @api private
115
+ # @param config [Config] The config to convert.
116
+ # @return [Hash{String => Object}] An +id+/+items+/+environments+ chain
117
+ # entry.
87
118
  def config_to_chain_entry(config)
88
119
  items_hash = config.items.to_h do |item|
89
120
  [item.name,
90
121
  { "value" => item.value, "type" => item.type, "description" => item.description }.compact]
91
122
  end
92
123
  environments = config.environments.each_with_object({}) do |(env_key, env_obj), out|
93
- # Per ADR-024 §2.4 env entries are flat +{key: rawValue}+ maps —
94
- # no +values+ envelope, no per-key type wrapper.
124
+ # Env entries are flat +{key: rawValue}+ maps — no +values+
125
+ # envelope, no per-key type wrapper.
95
126
  out[env_key] = env_obj.values
96
127
  end
97
128
  { "id" => config.id, "items" => items_hash, "environments" => environments }
@@ -101,13 +132,20 @@ module Smplkit
101
132
  #
102
133
  # Walks from root (last element) to child (first element), accumulating
103
134
  # values via deep merge so child configs override parent configs.
135
+ #
136
+ # @api private
137
+ # @param chain [Array<Hash{String => Object}>] Chain entries from child
138
+ # to root.
139
+ # @param environment [String, nil] The environment whose overrides to
140
+ # apply, or +nil+ for base values only.
141
+ # @return [Hash{String => Object}] The resolved +{key => value}+ map.
104
142
  def resolve_chain(chain, environment)
105
143
  accumulated = {}
106
144
  chain.reverse_each do |config_data|
107
145
  raw_items = config_data["items"] || config_data["values"] || {}
108
146
  base_values = unwrap_items(raw_items)
109
- # Per ADR-024 §2.4 env entries are flat +{key: rawValue}+ maps —
110
- # the resolver reads the env entry directly as the override map.
147
+ # Env entries are flat +{key: rawValue}+ maps — the resolver reads
148
+ # the env entry directly as the override map.
111
149
  env_data = (config_data["environments"] || {})[environment] || {}
112
150
  env_values = env_data.is_a?(Hash) ? env_data : {}
113
151
  config_resolved = deep_merge(base_values, env_values)
@@ -16,6 +16,13 @@ module Smplkit
16
16
  class ConfigItem
17
17
  attr_accessor :name, :value, :type, :description
18
18
 
19
+ # Create a typed config item.
20
+ #
21
+ # @param name [String] The item key within its config.
22
+ # @param value [Object] The item's value.
23
+ # @param type [String] The item value type — one of +"STRING"+,
24
+ # +"NUMBER"+, +"BOOLEAN"+, or +"JSON"+.
25
+ # @param description [String, nil] Optional human-readable description.
19
26
  def initialize(name:, value:, type:, description: nil)
20
27
  @name = name
21
28
  @value = value
@@ -23,6 +30,8 @@ module Smplkit
23
30
  @description = description
24
31
  end
25
32
 
33
+ # @return [Hash{String => Object}] The item as a plain Hash, omitting a
34
+ # +nil+ description.
26
35
  def to_h
27
36
  { "name" => @name, "value" => @value, "type" => @type, "description" => @description }.compact
28
37
  end
@@ -42,10 +51,13 @@ module Smplkit
42
51
  # setters with +environment:+ (e.g.
43
52
  # +cfg.set_string("k", "v", environment: "production")+).
44
53
  #
45
- # Per ADR-024 §2.4 the wire shape for env overrides is flat —
46
- # +{env: {key: rawValue}}+ so the in-memory representation is
47
- # simply a Hash of raw values, with no typed wrapper.
54
+ # An override stores only the raw value; the declared type and description
55
+ # come from the base item, so an item's type and description are ignored
56
+ # when an environment override is supplied.
48
57
  class ConfigEnvironment
58
+ # @param values [Hash{String => Object}, nil] Initial overrides as a flat
59
+ # +{key => raw_value}+ map. A legacy +{key => {"value" => raw}}+ wrapper
60
+ # is unwrapped to the raw value.
49
61
  def initialize(values: nil)
50
62
  @values_raw = {}
51
63
  return unless values
@@ -58,6 +70,9 @@ module Smplkit
58
70
  end
59
71
 
60
72
  # Returns overrides as a plain Hash +{ "key" => raw_value }+.
73
+ #
74
+ # @return [Hash{String => Object}] A read-only shallow copy of the
75
+ # overrides.
61
76
  def values
62
77
  @values_raw.dup
63
78
  end
@@ -65,6 +80,9 @@ module Smplkit
65
80
  # Returns overrides as a plain Hash +{ "key" => raw_value }+. Retained
66
81
  # as a separate accessor for backward compatibility; identical to
67
82
  # +values+ now that env overrides are stored flat.
83
+ #
84
+ # @return [Hash{String => Object}] A read-only shallow copy of the
85
+ # overrides.
68
86
  def values_raw
69
87
  @values_raw.dup
70
88
  end
@@ -82,6 +100,20 @@ module Smplkit
82
100
  class Config
83
101
  attr_accessor :id, :key, :name, :description, :parent_id, :created_at, :updated_at
84
102
 
103
+ # @param client [ConfigClient, nil] The owning client, or +nil+ for a
104
+ # detached model that cannot save or delete.
105
+ # @param key [String] The config identifier (slug).
106
+ # @param id [String, nil] The server-assigned id, or +nil+ for an unsaved
107
+ # config.
108
+ # @param name [String, nil] Display name.
109
+ # @param description [String, nil] Optional description.
110
+ # @param parent_id [String, nil] Parent config id (slug), or +nil+ for a
111
+ # root config.
112
+ # @param items [Array<ConfigItem>, nil] Base items.
113
+ # @param environments [Hash{String => ConfigEnvironment}, nil]
114
+ # Per-environment overrides keyed by environment id.
115
+ # @param created_at [Object, nil] Creation timestamp.
116
+ # @param updated_at [Object, nil] Last-modified timestamp.
85
117
  def initialize(client = nil, key:, id: nil, name: nil, description: nil,
86
118
  parent_id: nil, items: nil, environments: nil,
87
119
  created_at: nil, updated_at: nil)
@@ -97,14 +129,25 @@ module Smplkit
97
129
  @updated_at = updated_at
98
130
  end
99
131
 
132
+ # @return [Array<ConfigItem>] A read-only shallow copy of the base items.
100
133
  def items
101
134
  @items.dup
102
135
  end
103
136
 
137
+ # @return [Hash{String => ConfigEnvironment}] A read-only shallow copy of
138
+ # the per-environment overrides keyed by environment id.
104
139
  def environments
105
140
  @environments.dup
106
141
  end
107
142
 
143
+ # Persist this config to the server. Creates a new config if unsaved, or
144
+ # updates the existing one.
145
+ #
146
+ # @return [self]
147
+ # @raise [Smplkit::NotFoundError] If the config no longer exists
148
+ # (update).
149
+ # @raise [Smplkit::ValidationError] If the server rejects the request.
150
+ # @raise [RuntimeError] If the model was constructed without a client.
108
151
  def save
109
152
  raise "Config was constructed without a client; cannot save" if @client.nil?
110
153
 
@@ -119,6 +162,10 @@ module Smplkit
119
162
  end
120
163
  alias save! save
121
164
 
165
+ # Delete this config from the server.
166
+ #
167
+ # @return [void]
168
+ # @raise [RuntimeError] If the model was constructed without a client.
122
169
  def delete
123
170
  raise "Config was constructed without a client; cannot delete" if @client.nil?
124
171
 
@@ -126,26 +173,86 @@ module Smplkit
126
173
  end
127
174
  alias delete! delete
128
175
 
176
+ # Set (or replace) an item. When +environment:+ is given, sets an
177
+ # override on that environment.
178
+ #
179
+ # An environment override stores only the raw value; the declared type and
180
+ # description come from the base item, so the +ConfigItem+'s type and
181
+ # description are ignored when +environment:+ is supplied.
182
+ #
183
+ # @param item [ConfigItem] The item to set; its +name+ is the item key.
184
+ # @param environment [String, nil] When given, set the value as an
185
+ # override on this environment rather than on the base config.
186
+ # @return [self]
187
+ def set(item, environment: nil)
188
+ set_typed(item.name, item.value, item.type, environment: environment, description: item.description)
189
+ end
190
+
191
+ # Convenience: set a STRING item (or environment override).
192
+ #
193
+ # @param name [String] The item key to set.
194
+ # @param value [String] The string value.
195
+ # @param environment [String, nil] When given, set the value as an
196
+ # override on this environment rather than on the base config.
197
+ # @param description [String, nil] Optional human-readable description.
198
+ # Ignored when setting an environment override.
199
+ # @return [self]
129
200
  def set_string(name, value, environment: nil, description: nil)
130
201
  set_typed(name, value, ItemType::STRING, environment: environment, description: description)
131
202
  end
132
203
 
204
+ # Convenience: set a NUMBER item (or environment override).
205
+ #
206
+ # @param name [String] The item key to set.
207
+ # @param value [Integer, Float] The numeric value.
208
+ # @param environment [String, nil] When given, set the value as an
209
+ # override on this environment rather than on the base config.
210
+ # @param description [String, nil] Optional human-readable description.
211
+ # Ignored when setting an environment override.
212
+ # @return [self]
133
213
  def set_number(name, value, environment: nil, description: nil)
134
214
  set_typed(name, value, ItemType::NUMBER, environment: environment, description: description)
135
215
  end
136
216
 
217
+ # Convenience: set a BOOLEAN item (or environment override).
218
+ #
219
+ # @param name [String] The item key to set.
220
+ # @param value [Boolean] The boolean value.
221
+ # @param environment [String, nil] When given, set the value as an
222
+ # override on this environment rather than on the base config.
223
+ # @param description [String, nil] Optional human-readable description.
224
+ # Ignored when setting an environment override.
225
+ # @return [self]
137
226
  def set_boolean(name, value, environment: nil, description: nil)
138
227
  set_typed(name, value, ItemType::BOOLEAN, environment: environment, description: description)
139
228
  end
140
229
 
230
+ # Convenience: set a JSON item (or environment override).
231
+ #
232
+ # @param name [String] The item key to set.
233
+ # @param value [Object] Any JSON-serializable value (Hash, Array, or
234
+ # primitive).
235
+ # @param environment [String, nil] When given, set the value as an
236
+ # override on this environment rather than on the base config.
237
+ # @param description [String, nil] Optional human-readable description.
238
+ # Ignored when setting an environment override.
239
+ # @return [self]
141
240
  def set_json(name, value, environment: nil, description: nil)
142
241
  set_typed(name, value, ItemType::JSON, environment: environment, description: description)
143
242
  end
144
243
 
145
- def remove_item(name, environment: nil)
244
+ # Remove an item by name. When +environment:+ is given, removes the
245
+ # per-environment override only. Removing an item that isn't present is a
246
+ # no-op.
247
+ #
248
+ # @param name [String] The item key to remove.
249
+ # @param environment [String, nil] When given, remove only this
250
+ # environment's override for +name+, leaving the base item intact.
251
+ # @return [self]
252
+ def remove(name, environment: nil)
146
253
  if environment
147
254
  env = @environments[environment]
148
- return unless env
255
+ return self unless env
149
256
 
150
257
  raw = env.values_raw
151
258
  raw.delete(name)
@@ -181,8 +288,8 @@ module Smplkit
181
288
  @items << ConfigItem.new(name: name, value: value, type: type, description: description)
182
289
  end
183
290
  else
184
- # Per ADR-024 §2.4 env overrides carry the raw value only — type
185
- # and description live on the base item, not on the override.
291
+ # Env overrides carry the raw value only — type and description live
292
+ # on the base item, not on the override.
186
293
  env = (@environments[environment] ||= ConfigEnvironment.new)
187
294
  raw = env.values_raw
188
295
  raw[name] = value
@@ -4,6 +4,8 @@ require_relative "errors"
4
4
 
5
5
  module Smplkit
6
6
  # SDK configuration resolution: defaults -> file -> env vars -> constructor args.
7
+ #
8
+ # @api private
7
9
  module ConfigResolution
8
10
  CONFIG_KEYS = {
9
11
  "api_key" => "SMPLKIT_API_KEY",
@@ -33,8 +35,8 @@ module Smplkit
33
35
  keyword_init: true
34
36
  )
35
37
 
36
- ResolvedManagementConfig = Struct.new(
37
- :api_key, :base_domain, :scheme, :debug,
38
+ ResolvedClientConfig = Struct.new(
39
+ :api_key, :base_domain, :scheme, :debug, :extra_headers,
38
40
  keyword_init: true
39
41
  )
40
42
 
@@ -136,23 +138,29 @@ module Smplkit
136
138
  }
137
139
  ctor.each { |k, v| resolved[k] = v unless v.nil? }
138
140
 
139
- missing_required(resolved, "environment", active_profile)
140
- missing_required(resolved, "service", active_profile)
141
+ # Validate required fields.
142
+ #
143
+ # +environment+ and +service+ are OPTIONAL: an audit-only or jobs-only
144
+ # customer needs neither, and when +environment+ is absent the server
145
+ # derives it from the API key (the key can be scoped to an environment).
146
+ # config/flags/logging simply send no environment signal when it's unset.
147
+ # +api_key+ remains required.
141
148
  missing_required(resolved, "api_key", active_profile)
142
149
 
143
150
  ResolvedConfig.new(
144
151
  api_key: resolved["api_key"].to_s,
145
152
  base_domain: resolved["base_domain"].to_s,
146
153
  scheme: resolved["scheme"].to_s,
147
- environment: resolved["environment"].to_s,
148
- service: resolved["service"].to_s,
154
+ # Preserve nil rather than coercing to the literal string "".
155
+ environment: resolved["environment"]&.to_s,
156
+ service: resolved["service"]&.to_s,
149
157
  debug: resolved["debug"] ? true : false,
150
158
  telemetry: resolved["telemetry"] ? true : false
151
159
  )
152
160
  end
153
161
 
154
- def resolve_management_config(profile: nil, api_key: nil, base_domain: nil,
155
- scheme: nil, debug: nil, home_dir: nil)
162
+ def resolve_client_config(profile: nil, api_key: nil, base_domain: nil,
163
+ scheme: nil, debug: nil, home_dir: nil)
156
164
  resolved = {
157
165
  "api_key" => nil,
158
166
  "base_domain" => "smplkit.com",
@@ -185,7 +193,7 @@ module Smplkit
185
193
 
186
194
  missing_required(resolved, "api_key", active_profile)
187
195
 
188
- ResolvedManagementConfig.new(
196
+ ResolvedClientConfig.new(
189
197
  api_key: resolved["api_key"].to_s,
190
198
  base_domain: resolved["base_domain"].to_s,
191
199
  scheme: resolved["scheme"].to_s,
@@ -6,9 +6,9 @@ module Smplkit
6
6
  # A single error object from the server's JSON:API +errors+ array.
7
7
  #
8
8
  # +code+ is the application-specific machine-readable error code (e.g.
9
- # +environment_unmanaged+); per JSON:API §7 and ADR-014, smplkit sets
10
- # this on every error so callers can branch without string-matching
11
- # the human +detail+. +meta+ carries additional structured context
9
+ # +environment_unmanaged+); smplkit sets this on every error so callers
10
+ # can branch without string-matching the human +detail+. +meta+ carries
11
+ # additional structured context
12
12
  # (e.g. <tt>{"environment" => "staging"}</tt>).
13
13
  class ApiErrorDetail
14
14
  attr_reader :status, :code, :title, :detail, :source, :meta
@@ -62,6 +62,7 @@ module Smplkit
62
62
  end
63
63
  end
64
64
 
65
+ # @api private — Build a human-readable message from a list of error details.
65
66
  def self.derive_message(errors)
66
67
  return "An API error occurred" if errors.nil? || errors.empty?
67
68
 
@@ -84,6 +85,16 @@ module Smplkit
84
85
  # subscription plan does not include the required entitlement.
85
86
  class PaymentRequiredError < Error; end
86
87
 
88
+ # Raised when a logging operation is attempted before +install+.
89
+ #
90
+ # Smpl Logging monkey-patches the standard logging framework, so it stays
91
+ # opt-in: its live surface requires an explicit +LoggingClient#install+
92
+ # first. Config and flags connect lazily on first live use and never raise
93
+ # this.
94
+ class NotInstalledError < Error; end
95
+
96
+ # @api private — Internal helpers that parse JSON:API error bodies and map
97
+ # HTTP status codes onto the error classes above.
87
98
  module Errors
88
99
  module_function
89
100