liteguard 0.2.20260314

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,104 @@
1
+ module Liteguard
2
+ # Local rule evaluation engine — all evaluation is done without network calls.
3
+ module Evaluation
4
+ # Evaluate a guard against a property hash.
5
+ #
6
+ # Rules are processed in order and the first matching rule determines the
7
+ # result. If no enabled rule matches, the guard's configured default value
8
+ # is returned.
9
+ #
10
+ # @param guard [Guard] guard definition to evaluate
11
+ # @param properties [Hash{String => Object}] normalized evaluation properties
12
+ # @return [Boolean] the resolved open/closed result
13
+ def self.evaluate_guard(guard, properties)
14
+ guard.rules.each do |rule|
15
+ next unless rule.enabled
16
+ return rule.result if matches_rule?(rule, properties)
17
+ end
18
+ guard.default_value
19
+ end
20
+
21
+ # Determine whether a single rule matches the provided properties.
22
+ #
23
+ # @param rule [Rule] rule to evaluate
24
+ # @param props [Hash{String => Object}] normalized evaluation properties
25
+ # @return [Boolean] `true` when the rule matches
26
+ def self.matches_rule?(rule, props)
27
+ raw = props[rule.property_name]
28
+ return false if raw.nil?
29
+ return false if rule.values.empty?
30
+
31
+ first = rule.values.first
32
+
33
+ case rule.operator
34
+ when :equals then values_equal?(raw, first)
35
+ when :not_equals then !values_equal?(raw, first)
36
+ when :in then rule.values.any? { |v| values_equal?(raw, v) }
37
+ when :not_in then rule.values.none? { |v| values_equal?(raw, v) }
38
+ when :regex
39
+ pattern = first.to_s
40
+ begin
41
+ !!(raw.to_s =~ Regexp.new(pattern))
42
+ rescue RegexpError
43
+ false
44
+ end
45
+ when :gt then compare_ordered_values(raw, first)&.positive? || false
46
+ when :gte then (comparison = compare_ordered_values(raw, first)) && comparison >= 0 || false
47
+ when :lt then compare_ordered_values(raw, first)&.negative? || false
48
+ when :lte then (comparison = compare_ordered_values(raw, first)) && comparison <= 0 || false
49
+ else false
50
+ end
51
+ end
52
+ private_class_method :matches_rule?
53
+
54
+ # Determine whether two raw property values are equal without mixed-type coercion.
55
+ #
56
+ # @param a [Object] left-hand value
57
+ # @param b [Object] right-hand value
58
+ # @return [Boolean] `true` when values are equal and of the same kind
59
+ def self.values_equal?(a, b)
60
+ case a
61
+ when TrueClass, FalseClass
62
+ b == a
63
+ when String
64
+ b.is_a?(String) && a == b
65
+ else
66
+ na = to_numeric(a)
67
+ nb = to_numeric(b)
68
+ !na.nil? && !nb.nil? && na == nb
69
+ end
70
+ end
71
+ private_class_method :values_equal?
72
+
73
+ # Compare ordered values without mixed-type coercion.
74
+ #
75
+ # @param a [Object] left-hand value
76
+ # @param b [Object] right-hand value
77
+ # @return [-1, 0, 1, nil] comparison result compatible with `<=>`
78
+ def self.compare_ordered_values(a, b)
79
+ if a.is_a?(String) || b.is_a?(String)
80
+ return nil unless a.is_a?(String) && b.is_a?(String)
81
+
82
+ return a <=> b
83
+ end
84
+
85
+ na = to_numeric(a)
86
+ nb = to_numeric(b)
87
+ return nil if na.nil? || nb.nil?
88
+
89
+ na <=> nb
90
+ end
91
+ private_class_method :compare_ordered_values
92
+
93
+ # Coerce supported numeric values into a comparable form.
94
+ #
95
+ # @param v [Object] raw property value
96
+ # @return [Float, nil] numeric representation, or `nil` if coercion fails
97
+ def self.to_numeric(v)
98
+ case v
99
+ when Numeric then v.to_f
100
+ end
101
+ end
102
+ private_class_method :to_numeric
103
+ end
104
+ end
@@ -0,0 +1,159 @@
1
+ module Liteguard
2
+ # Immutable request scope for Liteguard evaluations.
3
+ class Scope
4
+ # @return [String] cache key for the guard bundle backing this scope
5
+ attr_reader :bundle_key
6
+
7
+ # Create a new immutable scope.
8
+ #
9
+ # Application code typically uses `Liteguard::Client#create_scope` instead of
10
+ # calling this constructor directly.
11
+ #
12
+ # @param client [Client] owning client instance
13
+ # @param properties [Hash{String => Object}] normalized scope properties
14
+ # @param bundle_key [String] cache key for the bundle used by this scope
15
+ # @param protected_context [ProtectedContext, nil] optional signed protected
16
+ # context associated with the scope
17
+ # @return [void]
18
+ def initialize(client, properties, bundle_key, protected_context)
19
+ @client = client
20
+ @properties = properties.dup.freeze
21
+ @bundle_key = bundle_key
22
+ @protected_context = protected_context ? ProtectedContext.new(
23
+ properties: protected_context.properties.dup,
24
+ signature: protected_context.signature.dup
25
+ ) : nil
26
+ end
27
+
28
+ # Return a new scope with merged properties.
29
+ #
30
+ # Existing properties are preserved unless overwritten by the new values.
31
+ #
32
+ # @param properties [Hash] properties to merge into the scope
33
+ # @return [Scope] a derived immutable scope
34
+ def with_properties(properties)
35
+ self.class.new(@client, @properties.merge(normalize_properties(properties)), @bundle_key, @protected_context)
36
+ end
37
+
38
+ # Alias-friendly wrapper for {#with_properties}.
39
+ #
40
+ # @param properties [Hash] properties to merge into the scope
41
+ # @return [Scope] a derived immutable scope
42
+ def add_properties(properties)
43
+ with_properties(properties)
44
+ end
45
+
46
+ # Return a new scope with the named properties removed.
47
+ #
48
+ # @param names [Array<String, Symbol>] property names to remove
49
+ # @return [Scope] a derived immutable scope
50
+ def clear_properties(names)
51
+ next_properties = @properties.dup
52
+ Array(names).each { |name| next_properties.delete(name.to_s) }
53
+ self.class.new(@client, next_properties, @bundle_key, @protected_context)
54
+ end
55
+
56
+ # Return a new scope with all properties cleared.
57
+ #
58
+ # @return [Scope] an empty derived scope
59
+ def reset_properties
60
+ self.class.new(@client, {}, @bundle_key, @protected_context)
61
+ end
62
+
63
+ # Return a new scope bound to a protected-context bundle.
64
+ #
65
+ # @param protected_context [ProtectedContext, Hash] signed protected context
66
+ # @return [Scope] a derived immutable scope
67
+ def bind_protected_context(protected_context)
68
+ @client.bind_protected_context_to_scope(self, protected_context)
69
+ end
70
+
71
+ # Return a new scope using the public guard bundle.
72
+ #
73
+ # @return [Scope] a derived immutable scope without protected context
74
+ def clear_protected_context
75
+ @client.ensure_public_bundle_ready
76
+ self.class.new(@client, @properties, Client::PUBLIC_BUNDLE_KEY, nil)
77
+ end
78
+
79
+ # Evaluate a guard within this scope and emit telemetry.
80
+ #
81
+ # @param name [String] guard name to evaluate
82
+ # @param options [Hash, nil] optional per-call overrides
83
+ # @return [Boolean] `true` when the guard resolves open
84
+ def is_open(name, options = nil, **legacy_options)
85
+ @client.is_open_in_scope(self, name, options, **legacy_options)
86
+ end
87
+
88
+ # Evaluate a guard within this scope without emitting telemetry.
89
+ #
90
+ # @param name [String] guard name to evaluate
91
+ # @param options [Hash, nil] optional per-call overrides
92
+ # @return [Boolean] `true` when the guard resolves open
93
+ def peek_is_open(name, options = nil, **legacy_options)
94
+ @client.peek_is_open_in_scope(self, name, options, **legacy_options)
95
+ end
96
+
97
+ # Evaluate a guard and run the block only when it is open.
98
+ #
99
+ # @param name [String] guard name to evaluate
100
+ # @param options [Hash, nil] optional per-call overrides
101
+ # @yield Runs only when the guard resolves open
102
+ # @return [Object, nil] the block return value, or `nil` when the guard is
103
+ # closed
104
+ def execute_if_open(name, options = nil, **legacy_options, &block)
105
+ @client.execute_if_open_in_scope(self, name, options, **legacy_options, &block)
106
+ end
107
+
108
+ # Bind this scope as the active scope for the duration of a block.
109
+ #
110
+ # @yield Runs with this scope bound as active
111
+ # @return [Object] the block return value
112
+ def run(&block)
113
+ @client.with_scope(self, &block)
114
+ end
115
+
116
+ # Run a block in this scope and attach a shared execution identifier.
117
+ #
118
+ # @yield Runs inside this scope and a correlated execution scope
119
+ # @return [Object] the block return value
120
+ def with_execution(&block)
121
+ @client.with_scope(self) { @client.with_execution(&block) }
122
+ end
123
+
124
+ # Return a defensive copy of the scope properties.
125
+ #
126
+ # @return [Hash] scope properties keyed by string
127
+ def properties
128
+ @properties.dup
129
+ end
130
+
131
+ # Return a defensive copy of the protected context, if present.
132
+ #
133
+ # @return [ProtectedContext, nil] copied protected context or `nil`
134
+ def protected_context
135
+ return nil unless @protected_context
136
+
137
+ ProtectedContext.new(
138
+ properties: @protected_context.properties.dup,
139
+ signature: @protected_context.signature.dup
140
+ )
141
+ end
142
+
143
+ # Determine whether this scope belongs to the provided client.
144
+ #
145
+ # @param client [Client] client to compare against
146
+ # @return [Boolean] `true` when both references point to the same client
147
+ def belongs_to?(client)
148
+ @client.equal?(client)
149
+ end
150
+
151
+ private
152
+
153
+ def normalize_properties(properties)
154
+ return {} if properties.nil?
155
+
156
+ properties.each_with_object({}) { |(key, value), acc| acc[key.to_s] = value }
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,134 @@
1
+ # Code generated by tools/src/proto-codegen.ts — DO NOT EDIT.
2
+ # Source of truth: proto/liteguard.proto Run: make proto
3
+
4
+ # Data-plane types — generated from proto/liteguard.proto.
5
+ module Liteguard
6
+
7
+ OPERATORS = %i[equals not_equals in not_in regex gt gte lt lte].freeze
8
+
9
+ # A single rule condition within a Guard.
10
+ Rule = Data.define(
11
+ :property_name,
12
+ :operator,
13
+ :values,
14
+ :result,
15
+ :enabled
16
+ )
17
+
18
+ # A named feature guard with an ordered rule set.
19
+ Guard = Data.define(
20
+ :name,
21
+ :rules,
22
+ :default_value,
23
+ :adopted,
24
+ :rate_limit_per_minute,
25
+ :rate_limit_properties,
26
+ :disable_measurement
27
+ )
28
+
29
+ # Keys accepted by the options hash passed to {Liteguard::Client#is_open}.
30
+ CHECK_OPTION_KEYS = %i[properties fallback disable_measurement].freeze
31
+
32
+ # ProtectedContext (mirrors proto message).
33
+ ProtectedContext = Data.define(
34
+ :properties,
35
+ :signature
36
+ )
37
+
38
+ # GetGuardsRequest (mirrors proto message).
39
+ GetGuardsRequest = Data.define(
40
+ :project_client_key_id,
41
+ :environment,
42
+ :protected_context
43
+ )
44
+
45
+ # Parsed response from the guards endpoint.
46
+ GuardsResponse = Data.define(
47
+ :guards,
48
+ :refresh_rate_seconds,
49
+ :etag
50
+ )
51
+
52
+ # OpenTelemetry trace correlation attached to a Signal.
53
+ TraceContext = Data.define(
54
+ :trace_id,
55
+ :span_id,
56
+ :parent_span_id
57
+ )
58
+
59
+ # GuardCheckPerformance (mirrors proto message).
60
+ GuardCheckPerformance = Data.define(
61
+ :rss_bytes,
62
+ :heap_used_bytes,
63
+ :heap_total_bytes,
64
+ :cpu_time_ns,
65
+ :gc_count,
66
+ :thread_count
67
+ )
68
+
69
+ # GuardExecutionPerformance (mirrors proto message).
70
+ GuardExecutionPerformance = Data.define(
71
+ :duration_ns,
72
+ :rss_end_bytes,
73
+ :heap_used_end_bytes,
74
+ :heap_total_end_bytes,
75
+ :cpu_time_end_ns,
76
+ :gc_count_end,
77
+ :thread_count_end,
78
+ :completed,
79
+ :error_class
80
+ )
81
+
82
+ # SignalPerformance (mirrors proto message).
83
+ SignalPerformance = Data.define(
84
+ :guard_check,
85
+ :guard_execution
86
+ )
87
+
88
+ # A buffered guard-check record for upload to the data plane.
89
+ Signal = Data.define(
90
+ :guard_name,
91
+ :result,
92
+ :properties,
93
+ :timestamp_ms,
94
+ :trace,
95
+ :signal_id,
96
+ :execution_id,
97
+ :parent_signal_id,
98
+ :sequence_number,
99
+ :callsite_id,
100
+ :kind,
101
+ :dropped_signals_since_last,
102
+ :measurement
103
+ )
104
+
105
+ # SendUnadoptedGuardsRequest (mirrors proto message).
106
+ SendUnadoptedGuardsRequest = Data.define(
107
+ :project_client_key_id,
108
+ :environment,
109
+ :guard_names
110
+ )
111
+
112
+ # SendUnadoptedGuardsResponse (mirrors proto message).
113
+ SendUnadoptedGuardsResponse = Data.define(
114
+ :accepted
115
+ )
116
+
117
+ # Default values for {Liteguard::Client} initialization options.
118
+ CLIENT_OPTION_DEFAULTS = {
119
+ project_client_key_id: nil,
120
+ environment: nil,
121
+ fallback: false,
122
+ refresh_rate_seconds: 30,
123
+ flush_rate_seconds: 10,
124
+ flush_size: 500,
125
+ backend_url: "https://api.liteguard.io",
126
+ quiet: true,
127
+ http_timeout_seconds: 4,
128
+ flush_buffer_multiplier:4,
129
+ disable_measurement: false,
130
+ }.freeze
131
+
132
+ CLIENT_OPTION_KEYS = CLIENT_OPTION_DEFAULTS.keys.freeze
133
+
134
+ end
data/lib/liteguard.rb ADDED
@@ -0,0 +1,19 @@
1
+ require_relative "liteguard/types"
2
+ require_relative "liteguard/evaluation"
3
+ require_relative "liteguard/scope"
4
+ require_relative "liteguard/client"
5
+
6
+ # Liteguard feature guard SDK for Ruby.
7
+ #
8
+ # require 'liteguard'
9
+ #
10
+ # client = Liteguard::Client.new('pckid-...', environment: 'production')
11
+ # client.start
12
+ # scope = client.create_scope(user_id: 'user-123', plan: 'pro')
13
+ #
14
+ # if scope.is_open('payments.checkout')
15
+ # # feature is enabled
16
+ # end
17
+ #
18
+ module Liteguard
19
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: liteguard
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.20260314
5
+ platform: ruby
6
+ authors:
7
+ - Liteguard
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-03-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.13'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.13'
27
+ - !ruby/object:Gem::Dependency
28
+ name: webmock
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.23'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.23'
41
+ description: Liteguard gives you feature guards, observability, and CVE auto-disable
42
+ in one gem. Guards are evaluated entirely in-process against rules fetched once
43
+ at startup — every open? check is sub-millisecond with no network round-trip.
44
+ email:
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - LICENSE
50
+ - README.md
51
+ - lib/liteguard.rb
52
+ - lib/liteguard/client.rb
53
+ - lib/liteguard/evaluation.rb
54
+ - lib/liteguard/scope.rb
55
+ - lib/liteguard/types.rb
56
+ homepage: https://liteguard.io
57
+ licenses:
58
+ - Apache-2.0
59
+ metadata:
60
+ homepage_uri: https://liteguard.io
61
+ source_code_uri: https://github.com/liteguard/liteguard/tree/main/sdk/ruby
62
+ documentation_uri: https://github.com/liteguard/liteguard/tree/main/sdk/ruby#readme
63
+ changelog_uri: https://github.com/liteguard/liteguard/releases
64
+ bug_tracker_uri: https://github.com/liteguard/liteguard/issues
65
+ rubygems_mfa_required: 'true'
66
+ post_install_message:
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '3.1'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubygems_version: 3.5.22
82
+ signing_key:
83
+ specification_version: 4
84
+ summary: Feature guards, observability, and security response in a single import
85
+ test_files: []