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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE +21 -0
- data/README.md +105 -0
- data/lib/smplkit/client.rb +218 -0
- data/lib/smplkit/config/client.rb +238 -0
- data/lib/smplkit/config/helpers.rb +108 -0
- data/lib/smplkit/config/models.rb +192 -0
- data/lib/smplkit/config_resolution.rb +202 -0
- data/lib/smplkit/context.rb +68 -0
- data/lib/smplkit/debug.rb +50 -0
- data/lib/smplkit/errors.rb +114 -0
- data/lib/smplkit/flags/client.rb +480 -0
- data/lib/smplkit/flags/helpers.rb +76 -0
- data/lib/smplkit/flags/models.rb +258 -0
- data/lib/smplkit/flags/types.rb +233 -0
- data/lib/smplkit/generators/install_generator.rb +42 -0
- data/lib/smplkit/helpers.rb +15 -0
- data/lib/smplkit/log_level.rb +57 -0
- data/lib/smplkit/logging/adapters/base.rb +63 -0
- data/lib/smplkit/logging/adapters/semantic_logger_adapter.rb +88 -0
- data/lib/smplkit/logging/adapters/stdlib_logger_adapter.rb +143 -0
- data/lib/smplkit/logging/client.rb +142 -0
- data/lib/smplkit/logging/helpers.rb +69 -0
- data/lib/smplkit/logging/levels.rb +86 -0
- data/lib/smplkit/logging/models.rb +124 -0
- data/lib/smplkit/logging/normalize.rb +16 -0
- data/lib/smplkit/logging/sources.rb +44 -0
- data/lib/smplkit/management/buffer.rb +111 -0
- data/lib/smplkit/management/client.rb +623 -0
- data/lib/smplkit/management/models.rb +133 -0
- data/lib/smplkit/management/types.rb +65 -0
- data/lib/smplkit/metrics.rb +78 -0
- data/lib/smplkit/railtie.rb +48 -0
- data/lib/smplkit/version.rb +5 -0
- data/lib/smplkit/ws.rb +92 -0
- data/lib/smplkit.rb +43 -0
- data/sig/smplkit.rbs +141 -0
- metadata +139 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smplkit
|
|
4
|
+
module Flags
|
|
5
|
+
# A constrained value entry on a +Flag+.
|
|
6
|
+
#
|
|
7
|
+
# Lives in +Flag#values+. Frozen — author values via +Flag#add_value+ /
|
|
8
|
+
# +Flag#remove_value+ / +Flag#clear_values+.
|
|
9
|
+
FlagValue = Data.define(:name, :value)
|
|
10
|
+
|
|
11
|
+
# A single targeting rule on a +Flag+.
|
|
12
|
+
#
|
|
13
|
+
# Lives in +FlagEnvironment#rules+. Frozen — author rules via the
|
|
14
|
+
# +Smplkit::Rule+ fluent builder and pass through +Flag#add_rule+.
|
|
15
|
+
#
|
|
16
|
+
# Attributes:
|
|
17
|
+
# - logic: JSON Logic predicate. Empty Hash means "always match".
|
|
18
|
+
# - value: Value to serve when +logic+ evaluates truthy.
|
|
19
|
+
# - description: Human-readable label (optional).
|
|
20
|
+
class FlagRule
|
|
21
|
+
attr_reader :logic, :value, :description
|
|
22
|
+
|
|
23
|
+
def initialize(logic:, value: nil, description: nil)
|
|
24
|
+
@logic = logic
|
|
25
|
+
@value = value
|
|
26
|
+
@description = description
|
|
27
|
+
freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ==(other)
|
|
31
|
+
other.is_a?(FlagRule) && logic == other.logic && value == other.value && description == other.description
|
|
32
|
+
end
|
|
33
|
+
alias eql? ==
|
|
34
|
+
|
|
35
|
+
def hash = [logic, value, description].hash
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Per-environment configuration on a +Flag+.
|
|
39
|
+
#
|
|
40
|
+
# Lives at +flag.environments[env_name]+. Frozen — mutate via +Flag#add_rule+
|
|
41
|
+
# / +Flag#enable_rules+ / +Flag#disable_rules+ / +Flag#set_default+ /
|
|
42
|
+
# +Flag#clear_rules+ (with +environment:+).
|
|
43
|
+
#
|
|
44
|
+
# Attributes:
|
|
45
|
+
# - enabled: Whether the flag is active in this environment.
|
|
46
|
+
# - default: Environment-specific default override (+nil+ means no override).
|
|
47
|
+
# - rules: Targeting rules to evaluate, in order.
|
|
48
|
+
class FlagEnvironment
|
|
49
|
+
attr_reader :enabled, :default, :rules
|
|
50
|
+
|
|
51
|
+
def initialize(enabled: true, default: nil, rules: [])
|
|
52
|
+
@enabled = enabled
|
|
53
|
+
@default = default
|
|
54
|
+
@rules = rules.is_a?(Array) ? rules.dup.freeze : rules
|
|
55
|
+
freeze
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def enabled? = @enabled
|
|
59
|
+
|
|
60
|
+
# Return a new FlagEnvironment with the given fields replaced.
|
|
61
|
+
def with(**changes)
|
|
62
|
+
self.class.new(
|
|
63
|
+
enabled: changes.fetch(:enabled, @enabled),
|
|
64
|
+
default: changes.fetch(:default, @default),
|
|
65
|
+
rules: changes.fetch(:rules, @rules)
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def ==(other)
|
|
70
|
+
other.is_a?(FlagEnvironment) && enabled == other.enabled && default == other.default && rules == other.rules
|
|
71
|
+
end
|
|
72
|
+
alias eql? ==
|
|
73
|
+
|
|
74
|
+
def hash = [enabled, default, rules].hash
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# A flag resource.
|
|
78
|
+
#
|
|
79
|
+
# Provides management operations (save, add_rule, environment settings)
|
|
80
|
+
# and runtime evaluation via +get+.
|
|
81
|
+
#
|
|
82
|
+
# Use typed variants (BooleanFlag, StringFlag, NumberFlag, JsonFlag)
|
|
83
|
+
# for type-safe +get+ return values.
|
|
84
|
+
class Flag
|
|
85
|
+
attr_accessor :id, :name, :type, :default, :description, :created_at, :updated_at
|
|
86
|
+
|
|
87
|
+
def initialize(client = nil, name:, type:, default:, id: nil, values: nil,
|
|
88
|
+
description: nil, environments: nil, created_at: nil, updated_at: nil)
|
|
89
|
+
@client = client
|
|
90
|
+
@id = id
|
|
91
|
+
@name = name
|
|
92
|
+
@type = type
|
|
93
|
+
@default = default
|
|
94
|
+
@values = values&.dup
|
|
95
|
+
@description = description
|
|
96
|
+
@environments = environments ? environments.dup : {}
|
|
97
|
+
@created_at = created_at
|
|
98
|
+
@updated_at = updated_at
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Read-only view of constrained values. +nil+ means unconstrained.
|
|
102
|
+
def values
|
|
103
|
+
@values&.dup
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Read-only view of per-environment configuration.
|
|
107
|
+
def environments
|
|
108
|
+
@environments.dup
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Persist this flag to the server.
|
|
112
|
+
#
|
|
113
|
+
# Creates a new flag if unsaved, or updates the existing one. Requires a
|
|
114
|
+
# management client (i.e. the flag was constructed via +mgmt.flags.new_*+
|
|
115
|
+
# or returned from +mgmt.flags.get/list+).
|
|
116
|
+
def save
|
|
117
|
+
raise "Flag was constructed without a client; cannot save" if @client.nil?
|
|
118
|
+
|
|
119
|
+
updated =
|
|
120
|
+
if @created_at.nil?
|
|
121
|
+
@client._create_flag(self)
|
|
122
|
+
else
|
|
123
|
+
@client._update_flag(self)
|
|
124
|
+
end
|
|
125
|
+
_apply(updated)
|
|
126
|
+
self
|
|
127
|
+
end
|
|
128
|
+
alias save! save
|
|
129
|
+
|
|
130
|
+
def delete
|
|
131
|
+
raise "Flag was constructed without a client or id; cannot delete" if @client.nil? || @id.nil?
|
|
132
|
+
|
|
133
|
+
@client.delete(@id)
|
|
134
|
+
end
|
|
135
|
+
alias delete! delete
|
|
136
|
+
|
|
137
|
+
# Append a rule to a specific environment.
|
|
138
|
+
#
|
|
139
|
+
# The +built_rule+ Hash must include an +"environment"+ key.
|
|
140
|
+
# Call +save+ to persist.
|
|
141
|
+
def add_rule(built_rule)
|
|
142
|
+
env_key = built_rule["environment"]
|
|
143
|
+
if env_key.nil?
|
|
144
|
+
raise ArgumentError,
|
|
145
|
+
"Built rule must include 'environment' key. " \
|
|
146
|
+
"Use Smplkit::Rule.new(..., environment: 'env_key').when(...).serve(...)"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
flag_rule = FlagRule.new(
|
|
150
|
+
logic: (built_rule["logic"] || {}).dup,
|
|
151
|
+
value: built_rule["value"],
|
|
152
|
+
description: built_rule["description"]
|
|
153
|
+
)
|
|
154
|
+
existing = @environments[env_key] || FlagEnvironment.new
|
|
155
|
+
@environments[env_key] = existing.with(rules: (existing.rules + [flag_rule]).freeze)
|
|
156
|
+
self
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def enable_rules(environment: nil)
|
|
160
|
+
scoped(environment) { |k| @environments[k] = (@environments[k] || FlagEnvironment.new).with(enabled: true) }
|
|
161
|
+
self
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def disable_rules(environment: nil)
|
|
165
|
+
scoped(environment) { |k| @environments[k] = (@environments[k] || FlagEnvironment.new).with(enabled: false) }
|
|
166
|
+
self
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def clear_rules(environment: nil)
|
|
170
|
+
scoped(environment) do |k|
|
|
171
|
+
@environments[k] = (@environments[k] || FlagEnvironment.new).with(rules: [].freeze)
|
|
172
|
+
end
|
|
173
|
+
self
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def set_default(value, environment:)
|
|
177
|
+
@environments[environment] = (@environments[environment] || FlagEnvironment.new).with(default: value)
|
|
178
|
+
self
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def clear_default(environment:)
|
|
182
|
+
@environments[environment] = (@environments[environment] || FlagEnvironment.new).with(default: nil)
|
|
183
|
+
self
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def add_value(flag_value)
|
|
187
|
+
@values ||= []
|
|
188
|
+
@values << flag_value
|
|
189
|
+
self
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def remove_value(name)
|
|
193
|
+
return self unless @values
|
|
194
|
+
|
|
195
|
+
@values = @values.reject { |v| v.name == name }
|
|
196
|
+
self
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def clear_values
|
|
200
|
+
@values = []
|
|
201
|
+
self
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def _apply(other)
|
|
205
|
+
@id = other.id
|
|
206
|
+
@name = other.name
|
|
207
|
+
@type = other.type
|
|
208
|
+
@default = other.default
|
|
209
|
+
@description = other.description
|
|
210
|
+
@values = other.instance_variable_get(:@values)&.dup
|
|
211
|
+
@environments = other.instance_variable_get(:@environments).dup
|
|
212
|
+
@created_at = other.created_at
|
|
213
|
+
@updated_at = other.updated_at
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
private
|
|
217
|
+
|
|
218
|
+
def scoped(environment, &)
|
|
219
|
+
if environment.nil?
|
|
220
|
+
@environments.each_key(&)
|
|
221
|
+
else
|
|
222
|
+
yield(environment)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
class BooleanFlag < Flag
|
|
228
|
+
def get(context: nil)
|
|
229
|
+
raw = @client._evaluate_handle(@id, @default, context)
|
|
230
|
+
!!raw
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
class StringFlag < Flag
|
|
235
|
+
def get(context: nil)
|
|
236
|
+
raw = @client._evaluate_handle(@id, @default, context)
|
|
237
|
+
raw.to_s
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
class NumberFlag < Flag
|
|
242
|
+
def get(context: nil)
|
|
243
|
+
@client._evaluate_handle(@id, @default, context)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
class JsonFlag < Flag
|
|
248
|
+
def get(context: nil)
|
|
249
|
+
@client._evaluate_handle(@id, @default, context)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Top-level re-exports for convenience.
|
|
255
|
+
FlagValue = Flags::FlagValue
|
|
256
|
+
FlagRule = Flags::FlagRule
|
|
257
|
+
FlagEnvironment = Flags::FlagEnvironment
|
|
258
|
+
end
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smplkit
|
|
4
|
+
# Operators supported by +Smplkit::Rule#when+.
|
|
5
|
+
#
|
|
6
|
+
# Customers should prefer +Smplkit::Op::EQ+ etc. over raw strings so the IDE
|
|
7
|
+
# can validate calls. Raw strings are still accepted for backward
|
|
8
|
+
# compatibility.
|
|
9
|
+
module Op
|
|
10
|
+
EQ = "=="
|
|
11
|
+
NEQ = "!="
|
|
12
|
+
LT = "<"
|
|
13
|
+
LTE = "<="
|
|
14
|
+
GT = ">"
|
|
15
|
+
GTE = ">="
|
|
16
|
+
IN = "in"
|
|
17
|
+
CONTAINS = "contains"
|
|
18
|
+
|
|
19
|
+
ALL = [EQ, NEQ, LT, LTE, GT, GTE, IN, CONTAINS].freeze
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# A typed entity referenced by targeting rules and registered with smplkit.
|
|
23
|
+
#
|
|
24
|
+
# Represents a single entity (user, account, device, etc.). The +type+ and
|
|
25
|
+
# +key+ identify the entity; +attributes+ (provided as a hash and/or keyword
|
|
26
|
+
# arguments) carry the data that targeting rules evaluate against.
|
|
27
|
+
#
|
|
28
|
+
# Used for both authoring (+flag.get(context: [...])+,
|
|
29
|
+
# +client.set_context([...])+, +mgmt.contexts.register([...])+) and reading
|
|
30
|
+
# (+mgmt.contexts.list/get+ return populated +Context+ instances with
|
|
31
|
+
# +save+ / +delete+ ready to call).
|
|
32
|
+
#
|
|
33
|
+
# Examples:
|
|
34
|
+
#
|
|
35
|
+
# Smplkit::Context.new("user", "user-123", plan: "enterprise")
|
|
36
|
+
# Smplkit::Context.new("account", "acme-corp", { "region" => "us" }, employee_count: 500)
|
|
37
|
+
class Context
|
|
38
|
+
CONTEXT_FIELDS = %i[type key name attributes created_at updated_at].freeze
|
|
39
|
+
|
|
40
|
+
attr_reader :type, :key, :attributes
|
|
41
|
+
attr_accessor :name, :created_at, :updated_at
|
|
42
|
+
|
|
43
|
+
def initialize(type, key, attributes = nil, name: nil, created_at: nil, updated_at: nil, **kwargs)
|
|
44
|
+
raise TypeError, "Context type must be a String, got #{type.class}: #{type.inspect}" unless type.is_a?(String)
|
|
45
|
+
unless key.is_a?(String)
|
|
46
|
+
raise TypeError,
|
|
47
|
+
"Context key must be a String, got #{key.class}: #{key.inspect}. " \
|
|
48
|
+
"If your identifier is numeric, stringify it at the SDK boundary."
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
@type = type
|
|
52
|
+
@key = key
|
|
53
|
+
@name = name
|
|
54
|
+
@attributes = stringify_keys(merge_attributes(attributes, kwargs))
|
|
55
|
+
@created_at = created_at
|
|
56
|
+
@updated_at = updated_at
|
|
57
|
+
@client = nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Composite "{type}:{key}" identifier.
|
|
61
|
+
def id
|
|
62
|
+
"#{@type}:#{@key}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Bulk-replace attributes. Forces String keys to match the wire format.
|
|
66
|
+
def attributes=(new_attrs)
|
|
67
|
+
@attributes = stringify_keys(new_attrs || {})
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Internal: associate a management client with this context so save/delete
|
|
71
|
+
# can route through it.
|
|
72
|
+
def _bind_client(client)
|
|
73
|
+
@client = client
|
|
74
|
+
self
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def _client
|
|
78
|
+
@client
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Persist this context to the server (create or update).
|
|
82
|
+
def save
|
|
83
|
+
raise "Context was constructed without a client; cannot save" if @client.nil?
|
|
84
|
+
|
|
85
|
+
updated = @client._save_context(self)
|
|
86
|
+
_apply(updated)
|
|
87
|
+
self
|
|
88
|
+
end
|
|
89
|
+
alias save! save
|
|
90
|
+
|
|
91
|
+
# Delete this context from the server.
|
|
92
|
+
def delete
|
|
93
|
+
raise "Context was constructed without a client; cannot delete" if @client.nil?
|
|
94
|
+
|
|
95
|
+
@client.delete(id)
|
|
96
|
+
end
|
|
97
|
+
alias delete! delete
|
|
98
|
+
|
|
99
|
+
def to_eval_hash
|
|
100
|
+
{ "key" => @key, **@attributes }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def ==(other)
|
|
104
|
+
other.is_a?(Context) && other.type == @type && other.key == @key && other.attributes == @attributes
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def hash
|
|
108
|
+
[@type, @key, @attributes].hash
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def eql?(other)
|
|
112
|
+
self == other
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def inspect
|
|
116
|
+
"#<Smplkit::Context type=#{@type.inspect} key=#{@key.inspect} " \
|
|
117
|
+
"name=#{@name.inspect} attributes=#{@attributes.inspect}>"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def _apply(other)
|
|
121
|
+
@type = other.type
|
|
122
|
+
@key = other.key
|
|
123
|
+
@name = other.name
|
|
124
|
+
@attributes = other.attributes.dup
|
|
125
|
+
@created_at = other.created_at
|
|
126
|
+
@updated_at = other.updated_at
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
def merge_attributes(attrs, kwargs)
|
|
132
|
+
base = attrs ? attrs.dup : {}
|
|
133
|
+
kwargs.each { |k, v| base[k] = v }
|
|
134
|
+
base
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def stringify_keys(hash)
|
|
138
|
+
hash.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Describes a flag declaration for buffered registration.
|
|
143
|
+
#
|
|
144
|
+
# Used by +Smplkit::ManagementClient#flags#register+ to queue declarations
|
|
145
|
+
# for bulk registration. +service+ and +environment+ default to +nil+; the
|
|
146
|
+
# runtime client fills them from the active +Smplkit::Client+ when it
|
|
147
|
+
# forwards declarations.
|
|
148
|
+
class FlagDeclaration
|
|
149
|
+
attr_reader :id, :type, :default, :service, :environment
|
|
150
|
+
|
|
151
|
+
def initialize(id:, type:, default:, service: nil, environment: nil)
|
|
152
|
+
@id = id
|
|
153
|
+
@type = type
|
|
154
|
+
@default = default
|
|
155
|
+
@service = service
|
|
156
|
+
@environment = environment
|
|
157
|
+
freeze
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def ==(other)
|
|
161
|
+
other.is_a?(FlagDeclaration) && id == other.id && type == other.type && default == other.default &&
|
|
162
|
+
service == other.service && environment == other.environment
|
|
163
|
+
end
|
|
164
|
+
alias eql? ==
|
|
165
|
+
|
|
166
|
+
def hash = [id, type, default, service, environment].hash
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Fluent builder for flag targeting rules.
|
|
170
|
+
#
|
|
171
|
+
# Smplkit::Rule.new("Enable for enterprise users", environment: "staging")
|
|
172
|
+
# .when("user.plan", Smplkit::Op::EQ, "enterprise")
|
|
173
|
+
# .serve(true)
|
|
174
|
+
#
|
|
175
|
+
# Multiple +.when+ calls are AND'd. +environment:+ is required so the target
|
|
176
|
+
# environment is unambiguous when the rule is passed to +Flag#add_rule+.
|
|
177
|
+
# +.serve+ finalizes the rule and returns the built Hash ready to pass to
|
|
178
|
+
# +add_rule+.
|
|
179
|
+
class Rule
|
|
180
|
+
def initialize(description, environment:)
|
|
181
|
+
@description = description
|
|
182
|
+
@environment = environment
|
|
183
|
+
@conditions = []
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Add a condition. Multiple calls are AND'd at the top level.
|
|
187
|
+
#
|
|
188
|
+
# Two forms:
|
|
189
|
+
# - +when(var, op, value)+ - convenience for simple comparisons.
|
|
190
|
+
# +op+ accepts an +Op+ constant (preferred) or a raw string
|
|
191
|
+
# (e.g. +"=="+, +"contains"+).
|
|
192
|
+
# - +when(expr)+ - escape hatch accepting an arbitrary JSON Logic
|
|
193
|
+
# expression (use this for OR, nested AND/OR, +if+, etc.).
|
|
194
|
+
# See https://jsonlogic.com/ for the full expression grammar.
|
|
195
|
+
def when(*args)
|
|
196
|
+
if args.length == 1 && args[0].is_a?(Hash)
|
|
197
|
+
@conditions << args[0]
|
|
198
|
+
return self
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
if args.length == 3
|
|
202
|
+
var, op, value = args
|
|
203
|
+
op_str = op.to_s
|
|
204
|
+
@conditions << if op_str == "contains"
|
|
205
|
+
{ "in" => [value, { "var" => var }] }
|
|
206
|
+
else
|
|
207
|
+
{ op_str => [{ "var" => var }, value] }
|
|
208
|
+
end
|
|
209
|
+
return self
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
raise ArgumentError,
|
|
213
|
+
"Rule#when takes either (var, op, value) or a single JSON Logic Hash; got args=#{args.inspect}"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Finalize the rule with +value+ served on match and return the built Hash.
|
|
217
|
+
def serve(value)
|
|
218
|
+
logic =
|
|
219
|
+
case @conditions.length
|
|
220
|
+
when 0 then {}
|
|
221
|
+
when 1 then @conditions[0]
|
|
222
|
+
else { "and" => @conditions }
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
{
|
|
226
|
+
"description" => @description,
|
|
227
|
+
"logic" => logic,
|
|
228
|
+
"value" => value,
|
|
229
|
+
"environment" => @environment
|
|
230
|
+
}
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module Smplkit
|
|
6
|
+
module Generators
|
|
7
|
+
# Generates +config/initializers/smplkit.rb+ for Rails apps.
|
|
8
|
+
#
|
|
9
|
+
# rails generate smplkit:install
|
|
10
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
def create_initializer_file
|
|
14
|
+
create_file "config/initializers/smplkit.rb", initializer_contents
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initializer_contents
|
|
18
|
+
<<~RUBY
|
|
19
|
+
# frozen_string_literal: true
|
|
20
|
+
|
|
21
|
+
# smplkit configuration. Anything you don't set here resolves through
|
|
22
|
+
# the standard SMPLKIT_* env vars or the ~/.smplkit profile file.
|
|
23
|
+
Rails.application.configure do
|
|
24
|
+
config.smplkit.environment = Rails.env
|
|
25
|
+
config.smplkit.service = "your-service-name"
|
|
26
|
+
# config.smplkit.api_key = ENV["SMPLKIT_API_KEY"]
|
|
27
|
+
|
|
28
|
+
# Optional: per-request context. The provider receives the Rack env
|
|
29
|
+
# and returns an Array of Smplkit::Context. Returning nil/[] is fine.
|
|
30
|
+
#
|
|
31
|
+
# config.smplkit.context_provider = ->(env) {
|
|
32
|
+
# user = env["warden"]&.user
|
|
33
|
+
# next [] unless user
|
|
34
|
+
#
|
|
35
|
+
# [Smplkit::Context.new("user", user.id.to_s, plan: user.plan)]
|
|
36
|
+
# }
|
|
37
|
+
end
|
|
38
|
+
RUBY
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smplkit
|
|
4
|
+
module Helpers
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
# Convert a slug-style key to a human-readable display name.
|
|
8
|
+
#
|
|
9
|
+
# key_to_display_name("checkout-v2") # => "Checkout V2"
|
|
10
|
+
# key_to_display_name("user_service") # => "User Service"
|
|
11
|
+
def key_to_display_name(key)
|
|
12
|
+
key.tr("-_", " ").split.map(&:capitalize).join(" ")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smplkit
|
|
4
|
+
# Log severity levels used by the Smpl Logging service.
|
|
5
|
+
#
|
|
6
|
+
# Acts as a string-valued enum: each constant equals its name when used in
|
|
7
|
+
# string contexts, and supports comparison via the +ordinal+.
|
|
8
|
+
class LogLevel
|
|
9
|
+
NAMES = %w[TRACE DEBUG INFO WARN ERROR FATAL SILENT].freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :name, :ordinal
|
|
12
|
+
|
|
13
|
+
def initialize(name, ordinal)
|
|
14
|
+
@name = name.freeze
|
|
15
|
+
@ordinal = ordinal
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_s = @name
|
|
20
|
+
def to_str = @name
|
|
21
|
+
def inspect = "#<Smplkit::LogLevel #{@name}>"
|
|
22
|
+
def ==(other) = other.is_a?(LogLevel) ? @ordinal == other.ordinal : @name == other
|
|
23
|
+
def hash = @ordinal.hash
|
|
24
|
+
def eql?(other) = self == other
|
|
25
|
+
def <=>(other) = other.is_a?(LogLevel) ? @ordinal <=> other.ordinal : nil
|
|
26
|
+
|
|
27
|
+
include Comparable
|
|
28
|
+
|
|
29
|
+
TRACE = new("TRACE", 0)
|
|
30
|
+
DEBUG = new("DEBUG", 1)
|
|
31
|
+
INFO = new("INFO", 2)
|
|
32
|
+
WARN = new("WARN", 3)
|
|
33
|
+
ERROR = new("ERROR", 4)
|
|
34
|
+
FATAL = new("FATAL", 5)
|
|
35
|
+
SILENT = new("SILENT", 6)
|
|
36
|
+
|
|
37
|
+
ALL = [TRACE, DEBUG, INFO, WARN, ERROR, FATAL, SILENT].freeze
|
|
38
|
+
|
|
39
|
+
BY_NAME = ALL.to_h { |lvl| [lvl.name, lvl] }.freeze
|
|
40
|
+
|
|
41
|
+
def self.from_string(value)
|
|
42
|
+
raise ArgumentError, "log level cannot be nil" if value.nil?
|
|
43
|
+
|
|
44
|
+
key = value.to_s.upcase
|
|
45
|
+
level = BY_NAME[key]
|
|
46
|
+
raise ArgumentError, "unknown log level: #{value.inspect}" unless level
|
|
47
|
+
|
|
48
|
+
level
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.coerce(value)
|
|
52
|
+
return value if value.is_a?(LogLevel)
|
|
53
|
+
|
|
54
|
+
from_string(value)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smplkit
|
|
4
|
+
module Logging
|
|
5
|
+
module Adapters
|
|
6
|
+
# Contract for pluggable logging framework integration.
|
|
7
|
+
#
|
|
8
|
+
# Adapters bridge the smplkit logging runtime to a specific logging
|
|
9
|
+
# framework (e.g., stdlib +Logger+, +semantic_logger+). Implement this
|
|
10
|
+
# interface to add support for a new logging framework.
|
|
11
|
+
class Base
|
|
12
|
+
# Human-readable adapter name for diagnostics
|
|
13
|
+
# (e.g., +"stdlib-logger"+).
|
|
14
|
+
def name
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Scan the runtime for existing loggers.
|
|
19
|
+
#
|
|
20
|
+
# Returns an Array of triples
|
|
21
|
+
# +[logger_name, explicit_level_or_nil, effective_level]+ where the
|
|
22
|
+
# levels are +Smplkit::LogLevel+ instances.
|
|
23
|
+
#
|
|
24
|
+
# - +explicit_level_or_nil+: the level the logger was explicitly
|
|
25
|
+
# set to, or +nil+ if it inherits from parent / framework default.
|
|
26
|
+
# - +effective_level+: the resolved level the framework uses for
|
|
27
|
+
# this logger, accounting for inheritance. Always non-nil.
|
|
28
|
+
def discover
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Set the level on a specific logger.
|
|
33
|
+
#
|
|
34
|
+
# +level+ is a +Smplkit::LogLevel+ instance; the adapter converts to
|
|
35
|
+
# its framework's native level representation.
|
|
36
|
+
def apply_level(_logger_name, _level)
|
|
37
|
+
raise NotImplementedError
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Install continuous discovery hook.
|
|
41
|
+
#
|
|
42
|
+
# The block is invoked with
|
|
43
|
+
# +(logger_name, explicit_level_or_nil, effective_level)+ whenever a
|
|
44
|
+
# new logger is created in the framework.
|
|
45
|
+
#
|
|
46
|
+
# May be a no-op if the framework doesn't support creation
|
|
47
|
+
# interception.
|
|
48
|
+
def install_hook(&)
|
|
49
|
+
raise NotImplementedError
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Remove the hook installed by +install_hook+. Called on
|
|
53
|
+
# +client.logging.close+.
|
|
54
|
+
def uninstall_hook
|
|
55
|
+
raise NotImplementedError
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Top-level re-export.
|
|
62
|
+
LoggingAdapter = Logging::Adapters::Base
|
|
63
|
+
end
|