smplkit 3.0.95 → 3.0.96

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.
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smplkit
4
+ module Platform
5
+ # Accept Color, hex string, or nil; reject anything else.
6
+ def self.coerce_color(value)
7
+ return value if value.nil? || value.is_a?(Color)
8
+ return Color.new(value) if value.is_a?(String)
9
+
10
+ raise TypeError, "color must be a Color, hex string, or nil; got #{value.class}: #{value.inspect}"
11
+ end
12
+
13
+ # Environment resource (sync). Mutate fields, then call +save+.
14
+ class Environment
15
+ attr_accessor :id, :name, :classification, :created_at, :updated_at
16
+ attr_reader :color
17
+
18
+ def initialize(client = nil, name:, id: nil, color: nil,
19
+ classification: EnvironmentClassification::STANDARD,
20
+ created_at: nil, updated_at: nil)
21
+ @client = client
22
+ @id = id
23
+ @name = name
24
+ @color = Platform.coerce_color(color)
25
+ @classification = classification
26
+ @created_at = created_at
27
+ @updated_at = updated_at
28
+ end
29
+
30
+ def color=(value)
31
+ @color = Platform.coerce_color(value)
32
+ end
33
+
34
+ # Create or update this environment on the server.
35
+ def save
36
+ raise "Environment was constructed without a client; cannot save" if @client.nil?
37
+
38
+ other = @created_at.nil? ? @client._create(self) : @client._update(self)
39
+ _apply(other)
40
+ self
41
+ end
42
+ alias save! save
43
+
44
+ # Delete this environment from the server.
45
+ def delete
46
+ raise "Environment was constructed without a client or id; cannot delete" if @client.nil? || @id.nil?
47
+
48
+ @client.delete(@id)
49
+ end
50
+ alias delete! delete
51
+
52
+ def to_s
53
+ "Environment(id=#{@id.inspect}, name=#{@name.inspect}, classification=#{@classification.inspect})"
54
+ end
55
+ alias inspect to_s
56
+
57
+ def _apply(other)
58
+ @id = other.id
59
+ @name = other.name
60
+ @color = other.color
61
+ @classification = other.classification
62
+ @created_at = other.created_at
63
+ @updated_at = other.updated_at
64
+ end
65
+ end
66
+
67
+ # Service resource (sync). Mutate fields, then call +save+.
68
+ class Service
69
+ attr_accessor :id, :name, :created_at, :updated_at
70
+
71
+ def initialize(client = nil, name:, id: nil, created_at: nil, updated_at: nil)
72
+ @client = client
73
+ @id = id
74
+ @name = name
75
+ @created_at = created_at
76
+ @updated_at = updated_at
77
+ end
78
+
79
+ # Create or update this service on the server.
80
+ def save
81
+ raise "Service was constructed without a client; cannot save" if @client.nil?
82
+
83
+ other = @created_at.nil? ? @client._create(self) : @client._update(self)
84
+ _apply(other)
85
+ self
86
+ end
87
+ alias save! save
88
+
89
+ # Delete this service from the server.
90
+ def delete
91
+ raise "Service was constructed without a client or id; cannot delete" if @client.nil? || @id.nil?
92
+
93
+ @client.delete(@id)
94
+ end
95
+ alias delete! delete
96
+
97
+ def to_s
98
+ "Service(id=#{@id.inspect}, name=#{@name.inspect})"
99
+ end
100
+ alias inspect to_s
101
+
102
+ def _apply(other)
103
+ @id = other.id
104
+ @name = other.name
105
+ @created_at = other.created_at
106
+ @updated_at = other.updated_at
107
+ end
108
+ end
109
+
110
+ # A context type resource (e.g. "user", "account").
111
+ class ContextType
112
+ attr_accessor :id, :name, :attributes, :created_at, :updated_at
113
+
114
+ def initialize(client = nil, name:, id: nil, attributes: nil, created_at: nil, updated_at: nil)
115
+ @client = client
116
+ @id = id
117
+ @name = name
118
+ @attributes = attributes ? deep_dup_attrs(attributes) : {}
119
+ @created_at = created_at
120
+ @updated_at = updated_at
121
+ end
122
+
123
+ # Add a known-attribute slot. Local; call +save+ to persist.
124
+ def add_attribute(name, **metadata)
125
+ @attributes[name] = stringify_meta(metadata)
126
+ end
127
+
128
+ # Remove a known-attribute slot. Local; call +save+ to persist.
129
+ def remove_attribute(name)
130
+ @attributes.delete(name)
131
+ end
132
+
133
+ # Replace a known-attribute slot's metadata. Local; call +save+.
134
+ def update_attribute(name, **metadata)
135
+ @attributes[name] = stringify_meta(metadata)
136
+ end
137
+
138
+ # Create or update this context type on the server.
139
+ def save
140
+ raise "ContextType was constructed without a client; cannot save" if @client.nil?
141
+
142
+ other = @created_at.nil? ? @client._create(self) : @client._update(self)
143
+ _apply(other)
144
+ self
145
+ end
146
+ alias save! save
147
+
148
+ # Delete this context type from the server.
149
+ def delete
150
+ raise "ContextType was constructed without a client or id; cannot delete" if @client.nil? || @id.nil?
151
+
152
+ @client.delete(@id)
153
+ end
154
+ alias delete! delete
155
+
156
+ def to_s
157
+ "ContextType(id=#{@id.inspect}, name=#{@name.inspect})"
158
+ end
159
+ alias inspect to_s
160
+
161
+ def _apply(other)
162
+ @id = other.id
163
+ @name = other.name
164
+ @attributes = deep_dup_attrs(other.attributes)
165
+ @created_at = other.created_at
166
+ @updated_at = other.updated_at
167
+ end
168
+
169
+ private
170
+
171
+ def stringify_meta(metadata)
172
+ metadata.each_with_object({}) { |(k, v), out| out[k.to_s] = v }
173
+ end
174
+
175
+ def deep_dup_attrs(attrs)
176
+ attrs.each_with_object({}) do |(k, v), out|
177
+ out[k.to_s] = v.is_a?(Hash) ? stringify_meta(v) : v
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Smplkit
4
- module Management
4
+ module Platform
5
5
  # Whether an environment participates in the canonical ordering.
6
6
  #
7
7
  # +STANDARD+ environments are the customer's deploy targets — production,
8
- # staging, development, etc.
8
+ # staging, development, etc. They participate in
9
+ # +account.settings.environment_order+ and appear in the standard Console
10
+ # environment columns.
11
+ #
9
12
  # +AD_HOC+ environments are transient targets (preview branches, individual
10
13
  # developer sandboxes) that should not appear in the standard ordering.
11
14
  module EnvironmentClassification
@@ -60,6 +63,6 @@ module Smplkit
60
63
  end
61
64
  end
62
65
 
63
- Color = Management::Color
64
- EnvironmentClassification = Management::EnvironmentClassification
66
+ Color = Platform::Color
67
+ EnvironmentClassification = Platform::EnvironmentClassification
65
68
  end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Smplkit
6
+ # Internal per-service HTTP transport construction.
7
+ #
8
+ # The top-level +Smplkit::Client+ needs one authenticated transport per
9
+ # backend service (app, config, flags, logging, jobs) plus a
10
+ # context-registration buffer that +client.platform+ owns. This module builds
11
+ # them in one place so the construction is side-effect-free (transports
12
+ # connect lazily on first call) and shared by the top-level client.
13
+ #
14
+ # There is no audit transport here — +client.audit+ owns its own.
15
+ module Transport
16
+ SDK_OWNED_HEADERS = %w[authorization content-type user-agent].freeze
17
+
18
+ module_function
19
+
20
+ # Project the runtime +ResolvedConfig+ down to the transport subset.
21
+ #
22
+ # The top-level client's resolved config is a superset of what the
23
+ # transports need; this drops the runtime-only fields (environment,
24
+ # service, telemetry).
25
+ def to_transport_config(cfg, extra_headers = nil)
26
+ ConfigResolution::ResolvedManagementConfig.new(
27
+ api_key: cfg.api_key,
28
+ base_domain: cfg.base_domain,
29
+ scheme: cfg.scheme,
30
+ debug: cfg.debug,
31
+ extra_headers: extra_headers
32
+ )
33
+ end
34
+
35
+ # The per-service authenticated transports built for a top-level client.
36
+ #
37
+ # Construction is side-effect-free: each transport connects lazily on its
38
+ # first call. +app_url+ is carried alongside so the account settings client
39
+ # and the WebSocket can reach the app service. +close+ tears down the
40
+ # underlying Faraday connection pools.
41
+ ServiceTransports = Struct.new(
42
+ :app_url, :api_key, :app_http, :config_http, :flags_http, :logging_http, :jobs_http,
43
+ keyword_init: true
44
+ ) do
45
+ def close
46
+ # The generated ApiClient owns Faraday connections that release on GC.
47
+ # No explicit shutdown is exposed; this stub keeps the API stable.
48
+ end
49
+ end
50
+
51
+ # Build the five per-service transports from a resolved transport config.
52
+ #
53
+ # Side-effect-free — the underlying Faraday clients are created lazily on
54
+ # the first request. Smpl Jobs is JSON:API, so its transport carries the
55
+ # +application/vnd.api+json+ Accept header.
56
+ def build_service_transports(cfg)
57
+ app_url = ConfigResolution.service_url(cfg.scheme, "app", cfg.base_domain)
58
+ ServiceTransports.new(
59
+ app_url: app_url,
60
+ api_key: cfg.api_key,
61
+ app_http: build_api_client(SmplkitGeneratedClient::App, "app", cfg),
62
+ config_http: build_api_client(SmplkitGeneratedClient::Config, "config", cfg),
63
+ flags_http: build_api_client(SmplkitGeneratedClient::Flags, "flags", cfg),
64
+ logging_http: build_api_client(SmplkitGeneratedClient::Logging, "logging", cfg),
65
+ jobs_http: build_api_client(SmplkitGeneratedClient::Jobs, "jobs", cfg,
66
+ accept: "application/vnd.api+json")
67
+ )
68
+ end
69
+
70
+ # Build a generated +ApiClient+ for one service from a resolved config.
71
+ #
72
+ # +base_url+, when supplied, overrides the +scheme+/+host+ derived from
73
+ # +subdomain+/+base_domain+ (the path a standalone product client takes
74
+ # when handed a fully-resolved app URL).
75
+ def build_api_client(generated_module, subdomain, cfg, accept: nil, base_url: nil)
76
+ configuration = generated_module::Configuration.new
77
+ if base_url.nil?
78
+ configuration.scheme = cfg.scheme
79
+ configuration.host = "#{subdomain}.#{cfg.base_domain}"
80
+ else
81
+ uri = URI.parse(base_url)
82
+ configuration.scheme = uri.scheme
83
+ port_suffix = uri.port && ![80, 443].include?(uri.port) ? ":#{uri.port}" : ""
84
+ configuration.host = "#{uri.host}#{port_suffix}"
85
+ end
86
+ configuration.base_path = ""
87
+ configuration.access_token = cfg.api_key
88
+ configuration.debugging = cfg.debug
89
+ HttpPool.configure(configuration)
90
+ generated_module::ApiClient.new(configuration).tap do |client|
91
+ client.default_headers["User-Agent"] = "smplkit-ruby-sdk/#{Smplkit::VERSION}"
92
+ client.default_headers["Accept"] = accept if accept
93
+ (cfg.extra_headers || {}).each do |k, v|
94
+ client.default_headers[k] = v unless SDK_OWNED_HEADERS.include?(k.downcase)
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
data/lib/smplkit.rb CHANGED
@@ -44,9 +44,25 @@ require_relative "smplkit/context"
44
44
  require_relative "smplkit/config_resolution"
45
45
  require_relative "smplkit/metrics"
46
46
  require_relative "smplkit/ws"
47
+
48
+ # Internal foundation shared by every product client.
49
+ require_relative "smplkit/buffers"
50
+ require_relative "smplkit/api_support"
51
+ require_relative "smplkit/transport"
52
+
53
+ # Flags types (incl. Context) load before platform, which references Context.
47
54
  require_relative "smplkit/flags/types"
48
55
  require_relative "smplkit/flags/models"
49
56
  require_relative "smplkit/flags/helpers"
57
+
58
+ # Platform + account (the cross-cutting surfaces) before the product clients
59
+ # that borrow them.
60
+ require_relative "smplkit/platform/types"
61
+ require_relative "smplkit/platform/models"
62
+ require_relative "smplkit/platform/client"
63
+ require_relative "smplkit/account/models"
64
+ require_relative "smplkit/account/client"
65
+
50
66
  require_relative "smplkit/flags/client"
51
67
  require_relative "smplkit/config/models"
52
68
  require_relative "smplkit/config/helpers"
@@ -66,14 +82,10 @@ require_relative "smplkit/audit/events"
66
82
  require_relative "smplkit/audit/resource_types"
67
83
  require_relative "smplkit/audit/event_types"
68
84
  require_relative "smplkit/audit/categories"
85
+ require_relative "smplkit/audit/forwarders"
69
86
  require_relative "smplkit/audit/client"
70
87
  require_relative "smplkit/jobs/models"
71
- require_relative "smplkit/management/types"
72
- require_relative "smplkit/management/models"
73
- require_relative "smplkit/management/buffer"
74
- require_relative "smplkit/management/audit"
75
- require_relative "smplkit/management/jobs"
76
- require_relative "smplkit/management/client"
88
+ require_relative "smplkit/jobs/client"
77
89
  require_relative "smplkit/client"
78
90
 
79
91
  require_relative "smplkit/railtie" if defined?(Rails::Railtie)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smplkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.95
4
+ version: 3.0.96
5
5
  platform: ruby
6
6
  authors:
7
7
  - Smpl Solutions LLC
@@ -879,13 +879,18 @@ files:
879
879
  - lib/smplkit/_generated/logging/spec/models/usage_list_response_spec.rb
880
880
  - lib/smplkit/_generated/logging/spec/models/usage_resource_spec.rb
881
881
  - lib/smplkit/_generated/logging/spec/spec_helper.rb
882
+ - lib/smplkit/account/client.rb
883
+ - lib/smplkit/account/models.rb
884
+ - lib/smplkit/api_support.rb
882
885
  - lib/smplkit/audit/buffer.rb
883
886
  - lib/smplkit/audit/categories.rb
884
887
  - lib/smplkit/audit/client.rb
885
888
  - lib/smplkit/audit/event_types.rb
886
889
  - lib/smplkit/audit/events.rb
890
+ - lib/smplkit/audit/forwarders.rb
887
891
  - lib/smplkit/audit/models.rb
888
892
  - lib/smplkit/audit/resource_types.rb
893
+ - lib/smplkit/buffers.rb
889
894
  - lib/smplkit/client.rb
890
895
  - lib/smplkit/config/client.rb
891
896
  - lib/smplkit/config/helpers.rb
@@ -901,6 +906,7 @@ files:
901
906
  - lib/smplkit/generators/install_generator.rb
902
907
  - lib/smplkit/helpers.rb
903
908
  - lib/smplkit/http_pool.rb
909
+ - lib/smplkit/jobs/client.rb
904
910
  - lib/smplkit/jobs/models.rb
905
911
  - lib/smplkit/log_level.rb
906
912
  - lib/smplkit/logging/adapters/base.rb
@@ -913,14 +919,12 @@ files:
913
919
  - lib/smplkit/logging/normalize.rb
914
920
  - lib/smplkit/logging/resolution.rb
915
921
  - lib/smplkit/logging/sources.rb
916
- - lib/smplkit/management/audit.rb
917
- - lib/smplkit/management/buffer.rb
918
- - lib/smplkit/management/client.rb
919
- - lib/smplkit/management/jobs.rb
920
- - lib/smplkit/management/models.rb
921
- - lib/smplkit/management/types.rb
922
922
  - lib/smplkit/metrics.rb
923
+ - lib/smplkit/platform/client.rb
924
+ - lib/smplkit/platform/models.rb
925
+ - lib/smplkit/platform/types.rb
923
926
  - lib/smplkit/railtie.rb
927
+ - lib/smplkit/transport.rb
924
928
  - lib/smplkit/version.rb
925
929
  - lib/smplkit/ws.rb
926
930
  - sig/smplkit.rbs
@@ -1,198 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Smplkit
4
- module Management
5
- CONTEXT_REGISTRATION_LRU_SIZE = 10_000
6
- CONTEXT_BATCH_FLUSH_SIZE = 100
7
- FLAG_BATCH_FLUSH_SIZE = 50
8
- LOGGER_BATCH_FLUSH_SIZE = 50
9
- CONFIG_BATCH_FLUSH_SIZE = 50
10
-
11
- # Thread-safe batch buffer for context registration.
12
- class ContextRegistrationBuffer
13
- def initialize
14
- @seen = {}
15
- @pending = []
16
- @lock = Mutex.new
17
- end
18
-
19
- def observe(contexts)
20
- @lock.synchronize do
21
- contexts.each do |ctx|
22
- cache_key = [ctx.type, ctx.key]
23
- next if @seen.key?(cache_key)
24
-
25
- @seen.shift if @seen.size >= CONTEXT_REGISTRATION_LRU_SIZE
26
- @seen[cache_key] = ctx.attributes
27
- @pending << { "type" => ctx.type, "key" => ctx.key, "attributes" => ctx.attributes.dup }
28
- end
29
- end
30
- end
31
-
32
- def drain
33
- @lock.synchronize do
34
- batch = @pending
35
- @pending = []
36
- batch
37
- end
38
- end
39
-
40
- def pending_count
41
- @lock.synchronize { @pending.length }
42
- end
43
- end
44
-
45
- # Thread-safe batch buffer for flag declarations.
46
- #
47
- # Use +peek+ + +commit(ids)+ for the send path so a failed POST leaves
48
- # declarations queued for the next attempt. +drain+ is unconditional and
49
- # used only by tests/teardown.
50
- class FlagRegistrationBuffer
51
- def initialize
52
- @seen = {}
53
- @pending = []
54
- @lock = Mutex.new
55
- end
56
-
57
- def add(declaration)
58
- @lock.synchronize do
59
- next if @seen.key?(declaration.id)
60
-
61
- @seen[declaration.id] = true
62
- item = { "id" => declaration.id, "type" => declaration.type, "default" => declaration.default }
63
- item["service"] = declaration.service if declaration.service
64
- item["environment"] = declaration.environment if declaration.environment
65
- @pending << item
66
- end
67
- end
68
-
69
- def peek
70
- @lock.synchronize { @pending.dup }
71
- end
72
-
73
- def commit(ids)
74
- return if ids.nil? || ids.empty?
75
-
76
- committed = ids.to_set
77
- @lock.synchronize { @pending.reject! { |item| committed.include?(item["id"]) } }
78
- end
79
-
80
- def drain
81
- @lock.synchronize do
82
- batch = @pending
83
- @pending = []
84
- batch
85
- end
86
- end
87
-
88
- def pending_count
89
- @lock.synchronize { @pending.length }
90
- end
91
- end
92
-
93
- # Thread-safe batch buffer for config declarations. Mirrors Python's
94
- # +_ConfigRegistrationBuffer+: per-config metadata is retained across
95
- # flushes so post-drain deltas re-attribute correctly, and items are
96
- # dedup'd per +(config_id, item_key)+ so an already-sent item is
97
- # never re-sent.
98
- class ConfigRegistrationBuffer
99
- def initialize
100
- @pending = {} # config_id -> { id:, items: {}, ...meta }
101
- @meta = {} # config_id -> { service:, environment:, parent:, name:, description: }
102
- @sent_items = {} # "#{config_id}::#{item_key}" -> true
103
- @lock = Mutex.new
104
- end
105
-
106
- # Idempotent — first writer's metadata wins.
107
- def declare(config_id, service:, environment:, parent: nil, name: nil, description: nil)
108
- @lock.synchronize do
109
- next if @meta.key?(config_id)
110
-
111
- @meta[config_id] = {
112
- service: service, environment: environment,
113
- parent: parent, name: name, description: description
114
- }
115
- @pending[config_id] = build_entry(config_id)
116
- end
117
- end
118
-
119
- # Queue an item declaration for an already-declared config. Items
120
- # already sent in a previous +drain+ are skipped.
121
- def add_item(config_id, item_key, item_type, default, description = nil)
122
- @lock.synchronize do
123
- next unless @meta.key?(config_id)
124
- next if @sent_items.key?("#{config_id}::#{item_key}")
125
-
126
- entry = (@pending[config_id] ||= build_entry(config_id))
127
- next if entry["items"].key?(item_key)
128
-
129
- item = { "value" => default, "type" => item_type }
130
- item["description"] = description unless description.nil?
131
- entry["items"][item_key] = item
132
- end
133
- end
134
-
135
- # Returns and clears the pending batch; records sent items.
136
- def drain
137
- @lock.synchronize do
138
- entries = @pending.values
139
- entries.each do |entry|
140
- entry["items"].each_key { |item_key| @sent_items["#{entry["id"]}::#{item_key}"] = true }
141
- end
142
- @pending = {}
143
- entries
144
- end
145
- end
146
-
147
- def pending_count
148
- @lock.synchronize { @pending.size }
149
- end
150
-
151
- private
152
-
153
- def build_entry(config_id)
154
- meta = @meta[config_id]
155
- entry = { "id" => config_id, "items" => {} }
156
- %i[service environment parent name description].each do |k|
157
- v = meta[k]
158
- entry[k.to_s] = v unless v.nil?
159
- end
160
- entry
161
- end
162
- end
163
-
164
- # Thread-safe batch buffer for logger discovery.
165
- class LoggerRegistrationBuffer
166
- def initialize
167
- @seen = {}
168
- @pending = []
169
- @lock = Mutex.new
170
- end
171
-
172
- def add(source)
173
- @lock.synchronize do
174
- next if @seen.key?(source.name)
175
-
176
- @seen[source.name] = source.resolved_level
177
- item = { "id" => source.name, "resolved_level" => source.resolved_level&.to_s }
178
- item["level"] = source.level&.to_s if source.level
179
- item["service"] = source.service if source.service
180
- item["environment"] = source.environment if source.environment
181
- @pending << item
182
- end
183
- end
184
-
185
- def drain
186
- @lock.synchronize do
187
- batch = @pending
188
- @pending = []
189
- batch
190
- end
191
- end
192
-
193
- def pending_count
194
- @lock.synchronize { @pending.length }
195
- end
196
- end
197
- end
198
- end