subflag-openfeature-provider 0.1.0
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/.rspec +3 -0
- data/.rspec_status +70 -0
- data/.rubocop.yml +21 -0
- data/README.md +116 -0
- data/Rakefile +11 -0
- data/lib/subflag/client.rb +124 -0
- data/lib/subflag/errors.rb +60 -0
- data/lib/subflag/evaluation_context.rb +72 -0
- data/lib/subflag/evaluation_result.rb +64 -0
- data/lib/subflag/provider.rb +213 -0
- data/lib/subflag/version.rb +5 -0
- data/lib/subflag.rb +50 -0
- data/subflag-openfeature-provider.gemspec +40 -0
- metadata +172 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 40e8537098d52a561735926123fc60c1ad200f3762151588e101f815447cf822
|
|
4
|
+
data.tar.gz: 42821e1d8a10786803898a2d17d1c82002f24a314351cb3b61aa7b43c0cca2ba
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8ae9b2658c93632d4cbcbee3af96c27fccda6c099f8f144a225b79961298f5851f7757c7404583bc235fc643f510ce0175fb579def7ebf285f9d97bb3f14cf67
|
|
7
|
+
data.tar.gz: e59f3c5b24a9a3f18fad1ceb09fb1d0dda9084f355abfd8d2862f3da81fcf4dc564dbf7143cbfa4fc948482a0efe69c8a94444f1b47c19d076ba5c0605fd0cd4
|
data/.rspec
ADDED
data/.rspec_status
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
example_id | status | run_time |
|
|
2
|
+
------------------------------------------------ | ------ | --------------- |
|
|
3
|
+
./spec/subflag/client_spec.rb[1:1:1] | passed | 0.00269 seconds |
|
|
4
|
+
./spec/subflag/client_spec.rb[1:1:2] | passed | 0.0005 seconds |
|
|
5
|
+
./spec/subflag/client_spec.rb[1:1:3] | passed | 0.00036 seconds |
|
|
6
|
+
./spec/subflag/client_spec.rb[1:1:4] | passed | 0.00034 seconds |
|
|
7
|
+
./spec/subflag/client_spec.rb[1:1:5] | passed | 0.00028 seconds |
|
|
8
|
+
./spec/subflag/client_spec.rb[1:2:1:1] | passed | 0.03835 seconds |
|
|
9
|
+
./spec/subflag/client_spec.rb[1:2:1:2] | passed | 0.01594 seconds |
|
|
10
|
+
./spec/subflag/client_spec.rb[1:2:1:3] | passed | 0.01299 seconds |
|
|
11
|
+
./spec/subflag/client_spec.rb[1:2:1:4] | passed | 0.01766 seconds |
|
|
12
|
+
./spec/subflag/client_spec.rb[1:2:1:5] | passed | 0.03152 seconds |
|
|
13
|
+
./spec/subflag/client_spec.rb[1:2:2:1] | passed | 0.02146 seconds |
|
|
14
|
+
./spec/subflag/client_spec.rb[1:2:3:1] | passed | 0.01918 seconds |
|
|
15
|
+
./spec/subflag/client_spec.rb[1:2:4:1] | passed | 0.01256 seconds |
|
|
16
|
+
./spec/subflag/client_spec.rb[1:2:4:2] | passed | 0.02079 seconds |
|
|
17
|
+
./spec/subflag/client_spec.rb[1:2:5:1] | passed | 0.01744 seconds |
|
|
18
|
+
./spec/subflag/client_spec.rb[1:2:6:1] | passed | 0.01392 seconds |
|
|
19
|
+
./spec/subflag/client_spec.rb[1:2:6:2] | passed | 0.01303 seconds |
|
|
20
|
+
./spec/subflag/client_spec.rb[1:2:7:1] | passed | 0.01581 seconds |
|
|
21
|
+
./spec/subflag/client_spec.rb[1:3:1:1] | passed | 0.01796 seconds |
|
|
22
|
+
./spec/subflag/client_spec.rb[1:3:1:2] | passed | 0.01345 seconds |
|
|
23
|
+
./spec/subflag/client_spec.rb[1:3:2:1] | passed | 0.01236 seconds |
|
|
24
|
+
./spec/subflag/errors_spec.rb[1:1] | passed | 0.00544 seconds |
|
|
25
|
+
./spec/subflag/errors_spec.rb[2:1] | passed | 0.0002 seconds |
|
|
26
|
+
./spec/subflag/evaluation_context_spec.rb[1:1:1] | passed | 0.00274 seconds |
|
|
27
|
+
./spec/subflag/evaluation_context_spec.rb[1:1:2] | passed | 0.00038 seconds |
|
|
28
|
+
./spec/subflag/evaluation_context_spec.rb[1:1:3] | passed | 0.00025 seconds |
|
|
29
|
+
./spec/subflag/evaluation_context_spec.rb[1:1:4] | passed | 0.00016 seconds |
|
|
30
|
+
./spec/subflag/evaluation_context_spec.rb[1:1:5] | passed | 0.00014 seconds |
|
|
31
|
+
./spec/subflag/evaluation_context_spec.rb[1:2:1] | passed | 0.00012 seconds |
|
|
32
|
+
./spec/subflag/evaluation_context_spec.rb[1:2:2] | passed | 0.00277 seconds |
|
|
33
|
+
./spec/subflag/evaluation_context_spec.rb[1:2:3] | passed | 0.00018 seconds |
|
|
34
|
+
./spec/subflag/evaluation_context_spec.rb[1:3:1] | passed | 0.00013 seconds |
|
|
35
|
+
./spec/subflag/evaluation_context_spec.rb[1:3:2] | passed | 0.00011 seconds |
|
|
36
|
+
./spec/subflag/evaluation_context_spec.rb[1:3:3] | passed | 0.00011 seconds |
|
|
37
|
+
./spec/subflag/evaluation_context_spec.rb[1:3:4] | passed | 0.00012 seconds |
|
|
38
|
+
./spec/subflag/evaluation_context_spec.rb[1:3:5] | passed | 0.02665 seconds |
|
|
39
|
+
./spec/subflag/evaluation_context_spec.rb[1:3:6] | passed | 0.00018 seconds |
|
|
40
|
+
./spec/subflag/evaluation_context_spec.rb[1:3:7] | passed | 0.00014 seconds |
|
|
41
|
+
./spec/subflag/evaluation_result_spec.rb[1:1:1] | passed | 0.00012 seconds |
|
|
42
|
+
./spec/subflag/evaluation_result_spec.rb[1:2:1] | passed | 0.0001 seconds |
|
|
43
|
+
./spec/subflag/evaluation_result_spec.rb[1:2:2] | passed | 0.0001 seconds |
|
|
44
|
+
./spec/subflag/provider_spec.rb[1:1:1] | passed | 0.0005 seconds |
|
|
45
|
+
./spec/subflag/provider_spec.rb[1:2:1] | passed | 0.00039 seconds |
|
|
46
|
+
./spec/subflag/provider_spec.rb[1:3:1] | passed | 0.00038 seconds |
|
|
47
|
+
./spec/subflag/provider_spec.rb[1:4:1:1] | passed | 0.0132 seconds |
|
|
48
|
+
./spec/subflag/provider_spec.rb[1:4:1:2] | passed | 0.01269 seconds |
|
|
49
|
+
./spec/subflag/provider_spec.rb[1:4:2:1] | passed | 0.01343 seconds |
|
|
50
|
+
./spec/subflag/provider_spec.rb[1:4:3:1] | passed | 0.01265 seconds |
|
|
51
|
+
./spec/subflag/provider_spec.rb[1:4:4:1] | passed | 0.01326 seconds |
|
|
52
|
+
./spec/subflag/provider_spec.rb[1:4:5:1] | passed | 0.0197 seconds |
|
|
53
|
+
./spec/subflag/provider_spec.rb[1:5:1] | passed | 0.01297 seconds |
|
|
54
|
+
./spec/subflag/provider_spec.rb[1:5:2] | passed | 0.01226 seconds |
|
|
55
|
+
./spec/subflag/provider_spec.rb[1:6:1] | passed | 0.01201 seconds |
|
|
56
|
+
./spec/subflag/provider_spec.rb[1:6:2] | passed | 0.01698 seconds |
|
|
57
|
+
./spec/subflag/provider_spec.rb[1:7:1] | passed | 0.01183 seconds |
|
|
58
|
+
./spec/subflag/provider_spec.rb[1:7:2] | passed | 0.01746 seconds |
|
|
59
|
+
./spec/subflag/provider_spec.rb[1:8:1] | passed | 0.01125 seconds |
|
|
60
|
+
./spec/subflag/provider_spec.rb[1:8:2] | passed | 0.0119 seconds |
|
|
61
|
+
./spec/subflag/provider_spec.rb[1:9:1] | passed | 0.01764 seconds |
|
|
62
|
+
./spec/subflag/provider_spec.rb[1:9:2] | passed | 0.01244 seconds |
|
|
63
|
+
./spec/subflag/provider_spec.rb[1:10:1] | passed | 0.01167 seconds |
|
|
64
|
+
./spec/subflag/provider_spec.rb[1:10:2] | passed | 0.01133 seconds |
|
|
65
|
+
./spec/subflag/provider_spec.rb[1:10:3] | passed | 0.01184 seconds |
|
|
66
|
+
./spec/subflag/provider_spec.rb[1:10:4] | passed | 0.01362 seconds |
|
|
67
|
+
./spec/subflag/provider_spec.rb[1:10:5] | passed | 0.01628 seconds |
|
|
68
|
+
./spec/subflag/provider_spec.rb[1:10:6] | passed | 0.0151 seconds |
|
|
69
|
+
./spec/subflag/provider_spec.rb[1:11:1] | passed | 0.01314 seconds |
|
|
70
|
+
./spec/subflag/provider_spec.rb[1:11:2] | passed | 0.01556 seconds |
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
TargetRubyVersion: 3.1
|
|
3
|
+
NewCops: enable
|
|
4
|
+
SuggestExtensions: false
|
|
5
|
+
|
|
6
|
+
Style/Documentation:
|
|
7
|
+
Enabled: false
|
|
8
|
+
|
|
9
|
+
Style/FrozenStringLiteralComment:
|
|
10
|
+
Enabled: true
|
|
11
|
+
|
|
12
|
+
Metrics/BlockLength:
|
|
13
|
+
Exclude:
|
|
14
|
+
- 'spec/**/*'
|
|
15
|
+
- '*.gemspec'
|
|
16
|
+
|
|
17
|
+
Metrics/MethodLength:
|
|
18
|
+
Max: 15
|
|
19
|
+
|
|
20
|
+
Layout/LineLength:
|
|
21
|
+
Max: 120
|
data/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Subflag OpenFeature Provider for Ruby
|
|
2
|
+
|
|
3
|
+
Ruby provider for [OpenFeature](https://openfeature.dev) that integrates with [Subflag](https://subflag.com) feature flag management.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
gem 'subflag-openfeature-provider'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or with Bundler:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bundle add subflag-openfeature-provider
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
### With OpenFeature SDK
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
require "openfeature/sdk"
|
|
23
|
+
require "subflag"
|
|
24
|
+
|
|
25
|
+
# Configure the provider
|
|
26
|
+
provider = Subflag::Provider.new(
|
|
27
|
+
api_url: ENV["SUBFLAG_API_URL"],
|
|
28
|
+
api_key: ENV["SUBFLAG_API_KEY"]
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
OpenFeature::SDK.configure do |config|
|
|
32
|
+
config.set_provider(provider)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Get a client and evaluate flags
|
|
36
|
+
client = OpenFeature::SDK.build_client
|
|
37
|
+
|
|
38
|
+
# Boolean flag
|
|
39
|
+
if client.fetch_boolean_value(flag_key: "dark-mode", default_value: false)
|
|
40
|
+
enable_dark_mode
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# String flag
|
|
44
|
+
theme = client.fetch_string_value(flag_key: "theme", default_value: "light")
|
|
45
|
+
|
|
46
|
+
# Number flag
|
|
47
|
+
limit = client.fetch_integer_value(flag_key: "rate-limit", default_value: 100)
|
|
48
|
+
|
|
49
|
+
# Object flag
|
|
50
|
+
config = client.fetch_object_value(flag_key: "feature-config", default_value: {})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### With Evaluation Context
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
context = {
|
|
57
|
+
targeting_key: "user-123",
|
|
58
|
+
plan: "premium",
|
|
59
|
+
country: "US"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
enabled = client.fetch_boolean_value(
|
|
63
|
+
flag_key: "premium-feature",
|
|
64
|
+
default_value: false,
|
|
65
|
+
evaluation_context: context
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Direct Client Usage
|
|
70
|
+
|
|
71
|
+
You can also use the Subflag client directly without OpenFeature:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
require "subflag"
|
|
75
|
+
|
|
76
|
+
client = Subflag::Client.new(
|
|
77
|
+
api_url: "https://api.subflag.com",
|
|
78
|
+
api_key: "sdk-production-abc123"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
result = client.evaluate("my-flag")
|
|
82
|
+
puts result.value # => true
|
|
83
|
+
puts result.variant # => "enabled"
|
|
84
|
+
puts result.reason # => "DEFAULT"
|
|
85
|
+
|
|
86
|
+
# Bulk evaluation
|
|
87
|
+
results = client.evaluate_all
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Configuration
|
|
91
|
+
|
|
92
|
+
| Option | Description | Default |
|
|
93
|
+
|--------|-------------|---------|
|
|
94
|
+
| `api_url` | Subflag API base URL | Required |
|
|
95
|
+
| `api_key` | SDK API key (`sdk-{env}-{random}`) | Required |
|
|
96
|
+
| `timeout` | Request timeout in seconds | 5 |
|
|
97
|
+
|
|
98
|
+
## Error Handling
|
|
99
|
+
|
|
100
|
+
The provider returns default values on errors, following OpenFeature conventions:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
result = client.fetch_boolean_value(flag_key: "unknown", default_value: false)
|
|
104
|
+
# result[:value] => false (default)
|
|
105
|
+
# result[:reason] => :error
|
|
106
|
+
# result[:error_code] => :flag_not_found
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Requirements
|
|
110
|
+
|
|
111
|
+
- Ruby >= 3.1
|
|
112
|
+
- openfeature-sdk >= 0.3
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
data/Rakefile
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Subflag
|
|
7
|
+
# HTTP client for communicating with the Subflag API
|
|
8
|
+
#
|
|
9
|
+
# @example Basic usage
|
|
10
|
+
# client = Client.new(api_url: "https://api.subflag.com", api_key: "sdk-dev-abc123")
|
|
11
|
+
# result = client.evaluate("my-flag")
|
|
12
|
+
#
|
|
13
|
+
# @example With evaluation context
|
|
14
|
+
# context = EvaluationContext.new(targeting_key: "user-123", attributes: { plan: "premium" })
|
|
15
|
+
# result = client.evaluate("my-flag", context: context)
|
|
16
|
+
class Client
|
|
17
|
+
DEFAULT_TIMEOUT = 5 # seconds
|
|
18
|
+
|
|
19
|
+
attr_reader :api_url, :api_key, :timeout
|
|
20
|
+
|
|
21
|
+
# @param api_url [String] The base URL of the Subflag API
|
|
22
|
+
# @param api_key [String] The SDK API key (format: sdk-{env}-{random})
|
|
23
|
+
# @param timeout [Integer] Request timeout in seconds (default: 5)
|
|
24
|
+
def initialize(api_url:, api_key:, timeout: DEFAULT_TIMEOUT)
|
|
25
|
+
@api_url = api_url.chomp("/")
|
|
26
|
+
@api_key = api_key
|
|
27
|
+
@timeout = timeout
|
|
28
|
+
@connection = build_connection
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Evaluate a single flag
|
|
32
|
+
#
|
|
33
|
+
# @param flag_key [String] The key of the flag to evaluate
|
|
34
|
+
# @param context [EvaluationContext, nil] Optional evaluation context
|
|
35
|
+
# @return [EvaluationResult] The evaluation result
|
|
36
|
+
# @raise [FlagNotFoundError] If the flag doesn't exist
|
|
37
|
+
# @raise [AuthenticationError] If the API key is invalid
|
|
38
|
+
# @raise [ApiError] For other API errors
|
|
39
|
+
def evaluate(flag_key, context: nil)
|
|
40
|
+
response = post("/sdk/evaluate/#{encode_uri_component(flag_key)}", context&.to_h)
|
|
41
|
+
EvaluationResult.from_response(response)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Evaluate all flags in the environment
|
|
45
|
+
#
|
|
46
|
+
# @param context [EvaluationContext, nil] Optional evaluation context
|
|
47
|
+
# @return [Array<EvaluationResult>] Array of evaluation results
|
|
48
|
+
# @raise [AuthenticationError] If the API key is invalid
|
|
49
|
+
# @raise [ApiError] For other API errors
|
|
50
|
+
def evaluate_all(context: nil)
|
|
51
|
+
response = post("/sdk/evaluate-all", context&.to_h)
|
|
52
|
+
response.map { |data| EvaluationResult.from_response(data) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def build_connection
|
|
58
|
+
Faraday.new(url: @api_url) do |conn|
|
|
59
|
+
conn.request :json
|
|
60
|
+
conn.response :json
|
|
61
|
+
conn.options.timeout = @timeout
|
|
62
|
+
conn.options.open_timeout = @timeout
|
|
63
|
+
conn.headers["Content-Type"] = "application/json"
|
|
64
|
+
conn.headers["X-Subflag-API-Key"] = @api_key
|
|
65
|
+
conn.adapter Faraday.default_adapter
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def post(path, body)
|
|
70
|
+
response = @connection.post(path) do |req|
|
|
71
|
+
req.body = body.to_json if body && !body.empty?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
handle_response(response, path)
|
|
75
|
+
rescue Faraday::TimeoutError => e
|
|
76
|
+
raise TimeoutError, "Request timed out after #{@timeout}s: #{e.message}"
|
|
77
|
+
rescue Faraday::ConnectionFailed => e
|
|
78
|
+
raise ConnectionError, "Failed to connect to #{@api_url}: #{e.message}"
|
|
79
|
+
rescue Faraday::Error => e
|
|
80
|
+
raise ApiError.new("HTTP request failed: #{e.message}")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def handle_response(response, path)
|
|
84
|
+
body = parse_body(response.body)
|
|
85
|
+
|
|
86
|
+
case response.status
|
|
87
|
+
when 200, 201
|
|
88
|
+
body
|
|
89
|
+
when 401, 403
|
|
90
|
+
raise AuthenticationError.new(
|
|
91
|
+
extract_message(body) || "Authentication failed",
|
|
92
|
+
status: response.status,
|
|
93
|
+
details: body
|
|
94
|
+
)
|
|
95
|
+
when 404
|
|
96
|
+
flag_key = path.split("/").last
|
|
97
|
+
raise FlagNotFoundError.new(flag_key, status: 404, details: body)
|
|
98
|
+
else
|
|
99
|
+
raise ApiError.new(
|
|
100
|
+
extract_message(body) || "API request failed with status #{response.status}",
|
|
101
|
+
status: response.status,
|
|
102
|
+
details: body
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def parse_body(body)
|
|
108
|
+
return body if body.is_a?(Hash) || body.is_a?(Array)
|
|
109
|
+
return {} if body.nil? || body.empty?
|
|
110
|
+
|
|
111
|
+
JSON.parse(body)
|
|
112
|
+
rescue JSON::ParserError
|
|
113
|
+
{ "message" => body }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def extract_message(body)
|
|
117
|
+
body["message"] if body.is_a?(Hash)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def encode_uri_component(str)
|
|
121
|
+
URI.encode_www_form_component(str.to_s)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Subflag
|
|
4
|
+
# Base error class for all Subflag errors
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when API request fails
|
|
8
|
+
class ApiError < Error
|
|
9
|
+
attr_reader :status, :details
|
|
10
|
+
|
|
11
|
+
def initialize(message, status: nil, details: nil)
|
|
12
|
+
@status = status
|
|
13
|
+
@details = details
|
|
14
|
+
super(message)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Raised when authentication fails (401/403)
|
|
19
|
+
class AuthenticationError < ApiError
|
|
20
|
+
def initialize(message = "Invalid or missing API key", **kwargs)
|
|
21
|
+
super(message, **kwargs)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Raised when a flag is not found (404)
|
|
26
|
+
class FlagNotFoundError < ApiError
|
|
27
|
+
attr_reader :flag_key
|
|
28
|
+
|
|
29
|
+
def initialize(flag_key, **kwargs)
|
|
30
|
+
@flag_key = flag_key
|
|
31
|
+
super("Flag not found: #{flag_key}", **kwargs)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Raised when flag value type doesn't match requested type
|
|
36
|
+
class TypeMismatchError < Error
|
|
37
|
+
attr_reader :flag_key, :expected_type, :actual_type
|
|
38
|
+
|
|
39
|
+
def initialize(flag_key, expected_type:, actual_type:)
|
|
40
|
+
@flag_key = flag_key
|
|
41
|
+
@expected_type = expected_type
|
|
42
|
+
@actual_type = actual_type
|
|
43
|
+
super("Type mismatch for flag '#{flag_key}': expected #{expected_type}, got #{actual_type}")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Raised when network/connection fails
|
|
48
|
+
class ConnectionError < Error
|
|
49
|
+
def initialize(message = "Failed to connect to Subflag API")
|
|
50
|
+
super(message)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Raised when request times out
|
|
55
|
+
class TimeoutError < Error
|
|
56
|
+
def initialize(message = "Request to Subflag API timed out")
|
|
57
|
+
super(message)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Subflag
|
|
4
|
+
# Represents the context for flag evaluation, including targeting information
|
|
5
|
+
# and custom attributes for segment matching and percentage rollouts.
|
|
6
|
+
#
|
|
7
|
+
# @example Basic usage with targeting key
|
|
8
|
+
# context = EvaluationContext.new(targeting_key: "user-123")
|
|
9
|
+
#
|
|
10
|
+
# @example With custom attributes
|
|
11
|
+
# context = EvaluationContext.new(
|
|
12
|
+
# targeting_key: "user-123",
|
|
13
|
+
# kind: "user",
|
|
14
|
+
# attributes: { plan: "premium", country: "US" }
|
|
15
|
+
# )
|
|
16
|
+
class EvaluationContext
|
|
17
|
+
attr_reader :targeting_key, :kind, :attributes
|
|
18
|
+
|
|
19
|
+
# @param targeting_key [String, nil] Unique identifier for targeting (user ID, session ID, etc.)
|
|
20
|
+
# @param kind [String, nil] The kind of context ("user", "organization", "device", etc.)
|
|
21
|
+
# @param attributes [Hash, nil] Custom attributes for targeting rules
|
|
22
|
+
def initialize(targeting_key: nil, kind: nil, attributes: nil)
|
|
23
|
+
@targeting_key = targeting_key
|
|
24
|
+
@kind = kind
|
|
25
|
+
@attributes = attributes || {}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Convert to hash for API request
|
|
29
|
+
# @return [Hash] The context as a hash
|
|
30
|
+
def to_h
|
|
31
|
+
{
|
|
32
|
+
targetingKey: @targeting_key,
|
|
33
|
+
kind: @kind,
|
|
34
|
+
attributes: @attributes.empty? ? nil : @attributes
|
|
35
|
+
}.compact
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Create from OpenFeature evaluation context
|
|
39
|
+
# @param openfeature_context [OpenFeature::SDK::EvaluationContext, Hash, nil]
|
|
40
|
+
# @return [EvaluationContext]
|
|
41
|
+
def self.from_openfeature(openfeature_context)
|
|
42
|
+
return new if openfeature_context.nil?
|
|
43
|
+
|
|
44
|
+
# Handle Hash-like context (OpenFeature context is typically a hash-like object)
|
|
45
|
+
if openfeature_context.respond_to?(:to_h)
|
|
46
|
+
ctx = openfeature_context.to_h
|
|
47
|
+
elsif openfeature_context.is_a?(Hash)
|
|
48
|
+
ctx = openfeature_context
|
|
49
|
+
else
|
|
50
|
+
return new
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Extract targeting_key (OpenFeature standard)
|
|
54
|
+
targeting_key = ctx[:targeting_key] || ctx["targeting_key"]
|
|
55
|
+
|
|
56
|
+
# Extract other attributes (excluding targeting_key)
|
|
57
|
+
attributes = ctx.reject { |k, _| [:targeting_key, "targeting_key"].include?(k) }
|
|
58
|
+
|
|
59
|
+
new(
|
|
60
|
+
targeting_key: targeting_key,
|
|
61
|
+
kind: "user", # Default to "user" kind for OpenFeature contexts
|
|
62
|
+
attributes: attributes.empty? ? nil : symbolize_keys(attributes)
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def self.symbolize_keys(hash)
|
|
69
|
+
hash.transform_keys(&:to_sym)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Subflag
|
|
4
|
+
# Represents the result of a flag evaluation from the Subflag API
|
|
5
|
+
class EvaluationResult
|
|
6
|
+
# Valid evaluation reasons
|
|
7
|
+
REASONS = %w[
|
|
8
|
+
DEFAULT
|
|
9
|
+
OVERRIDE
|
|
10
|
+
SEGMENT_MATCH
|
|
11
|
+
PERCENTAGE_ROLLOUT
|
|
12
|
+
TARGETING_MATCH
|
|
13
|
+
ERROR
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
attr_reader :flag_key, :value, :variant, :reason
|
|
17
|
+
|
|
18
|
+
# @param flag_key [String] The key of the evaluated flag
|
|
19
|
+
# @param value [Object] The evaluated value (type depends on flag configuration)
|
|
20
|
+
# @param variant [String] The name of the selected variant
|
|
21
|
+
# @param reason [String] Why this value was selected (one of REASONS)
|
|
22
|
+
def initialize(flag_key:, value:, variant:, reason:)
|
|
23
|
+
@flag_key = flag_key
|
|
24
|
+
@value = value
|
|
25
|
+
@variant = variant
|
|
26
|
+
@reason = reason
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Create from API response hash
|
|
30
|
+
# @param data [Hash] The API response data
|
|
31
|
+
# @return [EvaluationResult]
|
|
32
|
+
def self.from_response(data)
|
|
33
|
+
new(
|
|
34
|
+
flag_key: fetch_key(data, "flagKey"),
|
|
35
|
+
value: fetch_key(data, "value"),
|
|
36
|
+
variant: fetch_key(data, "variant"),
|
|
37
|
+
reason: fetch_key(data, "reason")
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Fetch a key from hash, checking both string and symbol keys
|
|
42
|
+
# Can't use || because false values would be skipped
|
|
43
|
+
def self.fetch_key(data, key)
|
|
44
|
+
data.key?(key) ? data[key] : data[key.to_sym]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Check if evaluation was successful (not an error)
|
|
48
|
+
# @return [Boolean]
|
|
49
|
+
def success?
|
|
50
|
+
reason != "ERROR"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Convert to hash
|
|
54
|
+
# @return [Hash]
|
|
55
|
+
def to_h
|
|
56
|
+
{
|
|
57
|
+
flag_key: @flag_key,
|
|
58
|
+
value: @value,
|
|
59
|
+
variant: @variant,
|
|
60
|
+
reason: @reason
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Subflag
|
|
4
|
+
# OpenFeature provider for Subflag feature flag management.
|
|
5
|
+
#
|
|
6
|
+
# This provider implements the OpenFeature provider interface using duck-typing,
|
|
7
|
+
# following the on-demand evaluation pattern (like the Node.js provider).
|
|
8
|
+
# Each flag evaluation makes an HTTP request to the Subflag API.
|
|
9
|
+
#
|
|
10
|
+
# @example Basic usage with OpenFeature
|
|
11
|
+
# require "openfeature/sdk"
|
|
12
|
+
# require "subflag"
|
|
13
|
+
#
|
|
14
|
+
# provider = Subflag::Provider.new(
|
|
15
|
+
# api_url: "https://api.subflag.com",
|
|
16
|
+
# api_key: "sdk-production-abc123"
|
|
17
|
+
# )
|
|
18
|
+
#
|
|
19
|
+
# OpenFeature::SDK.configure do |config|
|
|
20
|
+
# config.set_provider(provider)
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# client = OpenFeature::SDK.build_client
|
|
24
|
+
# enabled = client.fetch_boolean_value(flag_key: "dark-mode", default_value: false)
|
|
25
|
+
#
|
|
26
|
+
# @example With evaluation context
|
|
27
|
+
# context = { targeting_key: "user-123", plan: "premium" }
|
|
28
|
+
# enabled = client.fetch_boolean_value(
|
|
29
|
+
# flag_key: "premium-feature",
|
|
30
|
+
# default_value: false,
|
|
31
|
+
# evaluation_context: context
|
|
32
|
+
# )
|
|
33
|
+
class Provider
|
|
34
|
+
# Provider metadata for OpenFeature SDK
|
|
35
|
+
# @return [Hash] Provider metadata
|
|
36
|
+
def metadata
|
|
37
|
+
{ name: "Subflag Ruby Provider" }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @param api_url [String] The base URL of the Subflag API
|
|
41
|
+
# @param api_key [String] The SDK API key (format: sdk-{env}-{random})
|
|
42
|
+
# @param timeout [Integer] Request timeout in seconds (default: 5)
|
|
43
|
+
def initialize(api_url:, api_key:, timeout: Client::DEFAULT_TIMEOUT)
|
|
44
|
+
@client = Client.new(api_url: api_url, api_key: api_key, timeout: timeout)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Called when provider is registered with OpenFeature
|
|
48
|
+
# Named `init` instead of `initialize` to avoid Ruby constructor conflict
|
|
49
|
+
def init
|
|
50
|
+
# No-op for on-demand evaluation pattern
|
|
51
|
+
# Could be used for connection validation or pre-warming in the future
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Called when provider is unregistered or SDK is shutdown
|
|
55
|
+
def shutdown
|
|
56
|
+
# No-op for stateless HTTP client
|
|
57
|
+
# Could be used for connection pool cleanup in the future
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Evaluate a boolean flag
|
|
61
|
+
#
|
|
62
|
+
# @param flag_key [String] The flag key to evaluate
|
|
63
|
+
# @param default_value [Boolean] Value to return if evaluation fails
|
|
64
|
+
# @param evaluation_context [Hash, nil] Optional targeting context
|
|
65
|
+
# @return [Hash] Resolution details with :value, :reason, :variant, :error_code, :error_message
|
|
66
|
+
def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
|
|
67
|
+
evaluate_flag(flag_key, default_value, evaluation_context, :boolean)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Evaluate a string flag
|
|
71
|
+
#
|
|
72
|
+
# @param flag_key [String] The flag key to evaluate
|
|
73
|
+
# @param default_value [String] Value to return if evaluation fails
|
|
74
|
+
# @param evaluation_context [Hash, nil] Optional targeting context
|
|
75
|
+
# @return [Hash] Resolution details
|
|
76
|
+
def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
|
|
77
|
+
evaluate_flag(flag_key, default_value, evaluation_context, :string)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Evaluate a number flag (returns Float)
|
|
81
|
+
#
|
|
82
|
+
# @param flag_key [String] The flag key to evaluate
|
|
83
|
+
# @param default_value [Numeric] Value to return if evaluation fails
|
|
84
|
+
# @param evaluation_context [Hash, nil] Optional targeting context
|
|
85
|
+
# @return [Hash] Resolution details
|
|
86
|
+
def fetch_number_value(flag_key:, default_value:, evaluation_context: nil)
|
|
87
|
+
evaluate_flag(flag_key, default_value, evaluation_context, :number)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Evaluate an integer flag
|
|
91
|
+
#
|
|
92
|
+
# @param flag_key [String] The flag key to evaluate
|
|
93
|
+
# @param default_value [Integer] Value to return if evaluation fails
|
|
94
|
+
# @param evaluation_context [Hash, nil] Optional targeting context
|
|
95
|
+
# @return [Hash] Resolution details
|
|
96
|
+
def fetch_integer_value(flag_key:, default_value:, evaluation_context: nil)
|
|
97
|
+
evaluate_flag(flag_key, default_value, evaluation_context, :integer)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Evaluate a float flag
|
|
101
|
+
#
|
|
102
|
+
# @param flag_key [String] The flag key to evaluate
|
|
103
|
+
# @param default_value [Float] Value to return if evaluation fails
|
|
104
|
+
# @param evaluation_context [Hash, nil] Optional targeting context
|
|
105
|
+
# @return [Hash] Resolution details
|
|
106
|
+
def fetch_float_value(flag_key:, default_value:, evaluation_context: nil)
|
|
107
|
+
evaluate_flag(flag_key, default_value, evaluation_context, :float)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Evaluate an object/hash flag
|
|
111
|
+
#
|
|
112
|
+
# @param flag_key [String] The flag key to evaluate
|
|
113
|
+
# @param default_value [Hash] Value to return if evaluation fails
|
|
114
|
+
# @param evaluation_context [Hash, nil] Optional targeting context
|
|
115
|
+
# @return [Hash] Resolution details
|
|
116
|
+
def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
|
|
117
|
+
evaluate_flag(flag_key, default_value, evaluation_context, :object)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
# Core evaluation logic shared by all type-specific methods
|
|
123
|
+
def evaluate_flag(flag_key, default_value, evaluation_context, expected_type)
|
|
124
|
+
context = EvaluationContext.from_openfeature(evaluation_context)
|
|
125
|
+
result = @client.evaluate(flag_key, context: context)
|
|
126
|
+
|
|
127
|
+
# Validate type matches
|
|
128
|
+
unless type_matches?(result.value, expected_type)
|
|
129
|
+
return error_result(
|
|
130
|
+
default_value,
|
|
131
|
+
error_code: :type_mismatch,
|
|
132
|
+
error_message: "Flag '#{flag_key}' value type doesn't match requested type #{expected_type}"
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Convert value if needed (e.g., number -> integer)
|
|
137
|
+
converted_value = convert_value(result.value, expected_type)
|
|
138
|
+
|
|
139
|
+
{
|
|
140
|
+
value: converted_value,
|
|
141
|
+
reason: map_reason(result.reason),
|
|
142
|
+
variant: result.variant,
|
|
143
|
+
flag_metadata: { flag_key: result.flag_key }
|
|
144
|
+
}
|
|
145
|
+
rescue FlagNotFoundError => e
|
|
146
|
+
error_result(default_value, error_code: :flag_not_found, error_message: e.message)
|
|
147
|
+
rescue AuthenticationError => e
|
|
148
|
+
error_result(default_value, error_code: :invalid_context, error_message: e.message)
|
|
149
|
+
rescue TypeMismatchError => e
|
|
150
|
+
error_result(default_value, error_code: :type_mismatch, error_message: e.message)
|
|
151
|
+
rescue TimeoutError, ConnectionError => e
|
|
152
|
+
error_result(default_value, error_code: :general, error_message: e.message)
|
|
153
|
+
rescue StandardError => e
|
|
154
|
+
error_result(default_value, error_code: :general, error_message: "Unexpected error: #{e.message}")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Check if value matches expected type
|
|
158
|
+
def type_matches?(value, expected_type)
|
|
159
|
+
case expected_type
|
|
160
|
+
when :boolean
|
|
161
|
+
value == true || value == false
|
|
162
|
+
when :string
|
|
163
|
+
value.is_a?(String)
|
|
164
|
+
when :number, :integer, :float
|
|
165
|
+
value.is_a?(Numeric)
|
|
166
|
+
when :object
|
|
167
|
+
value.is_a?(Hash)
|
|
168
|
+
else
|
|
169
|
+
true
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Convert value to specific type if needed
|
|
174
|
+
def convert_value(value, expected_type)
|
|
175
|
+
case expected_type
|
|
176
|
+
when :integer
|
|
177
|
+
value.to_i
|
|
178
|
+
when :float
|
|
179
|
+
value.to_f
|
|
180
|
+
else
|
|
181
|
+
value
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Map Subflag reason to OpenFeature reason
|
|
186
|
+
def map_reason(subflag_reason)
|
|
187
|
+
case subflag_reason
|
|
188
|
+
when "DEFAULT"
|
|
189
|
+
:default
|
|
190
|
+
when "TARGETING_MATCH", "SEGMENT_MATCH"
|
|
191
|
+
:targeting_match
|
|
192
|
+
when "OVERRIDE"
|
|
193
|
+
:static
|
|
194
|
+
when "PERCENTAGE_ROLLOUT"
|
|
195
|
+
:split
|
|
196
|
+
when "ERROR"
|
|
197
|
+
:error
|
|
198
|
+
else
|
|
199
|
+
:unknown
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Build error result hash
|
|
204
|
+
def error_result(default_value, error_code:, error_message:)
|
|
205
|
+
{
|
|
206
|
+
value: default_value,
|
|
207
|
+
reason: :error,
|
|
208
|
+
error_code: error_code,
|
|
209
|
+
error_message: error_message
|
|
210
|
+
}
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
data/lib/subflag.rb
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "subflag/version"
|
|
4
|
+
require_relative "subflag/errors"
|
|
5
|
+
require_relative "subflag/evaluation_context"
|
|
6
|
+
require_relative "subflag/evaluation_result"
|
|
7
|
+
require_relative "subflag/client"
|
|
8
|
+
require_relative "subflag/provider"
|
|
9
|
+
|
|
10
|
+
# Subflag Ruby SDK for OpenFeature
|
|
11
|
+
#
|
|
12
|
+
# This module provides integration with Subflag feature flag management
|
|
13
|
+
# through the OpenFeature standard.
|
|
14
|
+
#
|
|
15
|
+
# @example Quick start with OpenFeature
|
|
16
|
+
# require "openfeature/sdk"
|
|
17
|
+
# require "subflag"
|
|
18
|
+
#
|
|
19
|
+
# # Configure the provider
|
|
20
|
+
# provider = Subflag::Provider.new(
|
|
21
|
+
# api_url: ENV["SUBFLAG_API_URL"],
|
|
22
|
+
# api_key: ENV["SUBFLAG_API_KEY"]
|
|
23
|
+
# )
|
|
24
|
+
#
|
|
25
|
+
# OpenFeature::SDK.configure do |config|
|
|
26
|
+
# config.set_provider(provider)
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# # Use the client
|
|
30
|
+
# client = OpenFeature::SDK.build_client
|
|
31
|
+
#
|
|
32
|
+
# if client.fetch_boolean_value(flag_key: "new-checkout", default_value: false)
|
|
33
|
+
# # New checkout flow
|
|
34
|
+
# else
|
|
35
|
+
# # Legacy checkout flow
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# @example Direct client usage (without OpenFeature)
|
|
39
|
+
# client = Subflag::Client.new(
|
|
40
|
+
# api_url: "https://api.subflag.com",
|
|
41
|
+
# api_key: "sdk-production-abc123"
|
|
42
|
+
# )
|
|
43
|
+
#
|
|
44
|
+
# result = client.evaluate("my-flag")
|
|
45
|
+
# puts result.value # => true
|
|
46
|
+
# puts result.variant # => "enabled"
|
|
47
|
+
# puts result.reason # => "DEFAULT"
|
|
48
|
+
#
|
|
49
|
+
module Subflag
|
|
50
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/subflag/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "subflag-openfeature-provider"
|
|
7
|
+
spec.version = Subflag::VERSION
|
|
8
|
+
spec.authors = ["Subflag"]
|
|
9
|
+
spec.email = ["support@subflag.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "OpenFeature provider for Subflag feature flag management"
|
|
12
|
+
spec.description = "A Ruby provider for OpenFeature that integrates with Subflag's feature flag management system. Supports boolean, string, number, and object flag types with evaluation context."
|
|
13
|
+
spec.homepage = "https://github.com/subflag/subflag"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
spec.required_ruby_version = ">= 3.1.0"
|
|
16
|
+
|
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
|
+
spec.metadata["source_code_uri"] = "https://github.com/subflag/subflag/tree/main/sdk/packages/openfeature-ruby-provider"
|
|
19
|
+
spec.metadata["changelog_uri"] = "https://github.com/subflag/subflag/blob/main/sdk/packages/openfeature-ruby-provider/CHANGELOG.md"
|
|
20
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
21
|
+
|
|
22
|
+
spec.files = Dir.chdir(__dir__) do
|
|
23
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
24
|
+
(File.expand_path(f) == __FILE__) ||
|
|
25
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
spec.require_paths = ["lib"]
|
|
29
|
+
|
|
30
|
+
# Runtime dependencies
|
|
31
|
+
spec.add_dependency "faraday", ">= 2.0", "< 3.0"
|
|
32
|
+
spec.add_dependency "openfeature-sdk", ">= 0.3", "< 1.0"
|
|
33
|
+
|
|
34
|
+
# Development dependencies
|
|
35
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
|
36
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
37
|
+
spec.add_development_dependency "rspec", "~> 3.12"
|
|
38
|
+
spec.add_development_dependency "rubocop", "~> 1.50"
|
|
39
|
+
spec.add_development_dependency "webmock", "~> 3.18"
|
|
40
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: subflag-openfeature-provider
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Subflag
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-11-30 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: faraday
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.0'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '3.0'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '2.0'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.0'
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: openfeature-sdk
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.3'
|
|
40
|
+
- - "<"
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '1.0'
|
|
43
|
+
type: :runtime
|
|
44
|
+
prerelease: false
|
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '0.3'
|
|
50
|
+
- - "<"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '1.0'
|
|
53
|
+
- !ruby/object:Gem::Dependency
|
|
54
|
+
name: bundler
|
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - "~>"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '2.0'
|
|
60
|
+
type: :development
|
|
61
|
+
prerelease: false
|
|
62
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - "~>"
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '2.0'
|
|
67
|
+
- !ruby/object:Gem::Dependency
|
|
68
|
+
name: rake
|
|
69
|
+
requirement: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - "~>"
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '13.0'
|
|
74
|
+
type: :development
|
|
75
|
+
prerelease: false
|
|
76
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - "~>"
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '13.0'
|
|
81
|
+
- !ruby/object:Gem::Dependency
|
|
82
|
+
name: rspec
|
|
83
|
+
requirement: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - "~>"
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '3.12'
|
|
88
|
+
type: :development
|
|
89
|
+
prerelease: false
|
|
90
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - "~>"
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '3.12'
|
|
95
|
+
- !ruby/object:Gem::Dependency
|
|
96
|
+
name: rubocop
|
|
97
|
+
requirement: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - "~>"
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '1.50'
|
|
102
|
+
type: :development
|
|
103
|
+
prerelease: false
|
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - "~>"
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: '1.50'
|
|
109
|
+
- !ruby/object:Gem::Dependency
|
|
110
|
+
name: webmock
|
|
111
|
+
requirement: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - "~>"
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: '3.18'
|
|
116
|
+
type: :development
|
|
117
|
+
prerelease: false
|
|
118
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
119
|
+
requirements:
|
|
120
|
+
- - "~>"
|
|
121
|
+
- !ruby/object:Gem::Version
|
|
122
|
+
version: '3.18'
|
|
123
|
+
description: A Ruby provider for OpenFeature that integrates with Subflag's feature
|
|
124
|
+
flag management system. Supports boolean, string, number, and object flag types
|
|
125
|
+
with evaluation context.
|
|
126
|
+
email:
|
|
127
|
+
- support@subflag.com
|
|
128
|
+
executables: []
|
|
129
|
+
extensions: []
|
|
130
|
+
extra_rdoc_files: []
|
|
131
|
+
files:
|
|
132
|
+
- ".rspec"
|
|
133
|
+
- ".rspec_status"
|
|
134
|
+
- ".rubocop.yml"
|
|
135
|
+
- README.md
|
|
136
|
+
- Rakefile
|
|
137
|
+
- lib/subflag.rb
|
|
138
|
+
- lib/subflag/client.rb
|
|
139
|
+
- lib/subflag/errors.rb
|
|
140
|
+
- lib/subflag/evaluation_context.rb
|
|
141
|
+
- lib/subflag/evaluation_result.rb
|
|
142
|
+
- lib/subflag/provider.rb
|
|
143
|
+
- lib/subflag/version.rb
|
|
144
|
+
- subflag-openfeature-provider.gemspec
|
|
145
|
+
homepage: https://github.com/subflag/subflag
|
|
146
|
+
licenses:
|
|
147
|
+
- MIT
|
|
148
|
+
metadata:
|
|
149
|
+
homepage_uri: https://github.com/subflag/subflag
|
|
150
|
+
source_code_uri: https://github.com/subflag/subflag/tree/main/sdk/packages/openfeature-ruby-provider
|
|
151
|
+
changelog_uri: https://github.com/subflag/subflag/blob/main/sdk/packages/openfeature-ruby-provider/CHANGELOG.md
|
|
152
|
+
rubygems_mfa_required: 'true'
|
|
153
|
+
post_install_message:
|
|
154
|
+
rdoc_options: []
|
|
155
|
+
require_paths:
|
|
156
|
+
- lib
|
|
157
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
158
|
+
requirements:
|
|
159
|
+
- - ">="
|
|
160
|
+
- !ruby/object:Gem::Version
|
|
161
|
+
version: 3.1.0
|
|
162
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
163
|
+
requirements:
|
|
164
|
+
- - ">="
|
|
165
|
+
- !ruby/object:Gem::Version
|
|
166
|
+
version: '0'
|
|
167
|
+
requirements: []
|
|
168
|
+
rubygems_version: 3.5.22
|
|
169
|
+
signing_key:
|
|
170
|
+
specification_version: 4
|
|
171
|
+
summary: OpenFeature provider for Subflag feature flag management
|
|
172
|
+
test_files: []
|