flagdash 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/LICENSE +9 -0
- data/README.md +187 -0
- data/lib/flagdash/version.rb +3 -0
- data/lib/flagdash.rb +189 -0
- metadata +49 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3fdf5074eeb7ba8c22b3a292be35ddd6fd8cabd0e6e81205d3da171435363534
|
|
4
|
+
data.tar.gz: 36dd77ce355adba2e9d4d1efb20e8301372789f09dd91e3cf6e44230e572e1a4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9beb6ee3cd7499008d39b34bf4fe54e7481664bd2eec5805b15dfd317020114a1da3373b032c6450ac191b1fdd954031bbb63a77c96af4c71ed5758c8a4a5da5
|
|
7
|
+
data.tar.gz: 4d963f879cd9650135a689dfed75f5f1f018b7edd6a7d5bfe8d60b722d1523ecf7120f40c8d313d17ce9ba82435fe883c4788b6afaf5fdb06344d121a71ecd03
|
data/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FlagDash
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# FlagDash Ruby SDK
|
|
2
|
+
|
|
3
|
+
Feature flags, remote config, AI configs, translations and experiments for
|
|
4
|
+
Ruby 3.1+.
|
|
5
|
+
|
|
6
|
+
Standard library only — no runtime dependencies.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
gem "flagdash"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or straight from GitHub, pinned to an immutable tag:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
gem "flagdash", github: "flagdash/flagdash-ruby", tag: "v0.1.0"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
require "flagdash"
|
|
24
|
+
|
|
25
|
+
client = FlagDash::Client.new(sdk_key: ENV.fetch("FLAGDASH_SDK_KEY"))
|
|
26
|
+
|
|
27
|
+
if client.flag("checkout-v2", default: false, context: {user_id: "alice"})
|
|
28
|
+
# new checkout
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
client.close
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## API key tiers
|
|
35
|
+
|
|
36
|
+
The key decides which project and environment you read, and what you may
|
|
37
|
+
reach. There is no `environment` argument anywhere in this SDK — the key
|
|
38
|
+
carries it.
|
|
39
|
+
|
|
40
|
+
| Key | Prefix | Reaches |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| Client | `pk_` | Flag values and configs. Safe in a browser or mobile app. |
|
|
43
|
+
| Server | `sk_` | The above, plus targeting rules, translations and experiments. Keep it server-side. |
|
|
44
|
+
|
|
45
|
+
## Configuration
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
client = FlagDash::Client.new(
|
|
49
|
+
sdk_key: ENV.fetch("FLAGDASH_SDK_KEY"),
|
|
50
|
+
base_url: "https://flagdash.io", # self-hosted? point it here
|
|
51
|
+
timeout: 5, # seconds per request
|
|
52
|
+
cache_ttl: 60, # seconds; 0 disables caching
|
|
53
|
+
region: "eu-west-1", # omit to auto-detect
|
|
54
|
+
transport: nil # inject for tests
|
|
55
|
+
)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`region` is detected from `FLY_REGION`, `AWS_REGION` and friends when omitted,
|
|
59
|
+
so region-scoped targeting works with no wiring.
|
|
60
|
+
|
|
61
|
+
## Feature flags
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
# One flag, with the fallback used whenever FlagDash cannot be reached.
|
|
65
|
+
client.flag("checkout-v2", default: false, context: {user_id: "alice"})
|
|
66
|
+
|
|
67
|
+
# Every flag for this context in one request.
|
|
68
|
+
client.all_flags(context: {user_id: "alice", country: "GB"})
|
|
69
|
+
|
|
70
|
+
# Why did it resolve that way?
|
|
71
|
+
detail = client.flag_detail("checkout-v2", context: {user_id: "alice"})
|
|
72
|
+
detail.value # => true
|
|
73
|
+
detail.reason # => "rule_match"
|
|
74
|
+
detail.variation # => "treatment"
|
|
75
|
+
|
|
76
|
+
# Flag metadata without evaluating anything.
|
|
77
|
+
client.list_flags
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Context
|
|
81
|
+
|
|
82
|
+
Both shapes work. A nested `user:` hash is flattened — `id` becomes `user_id`
|
|
83
|
+
and every other key becomes `user_<key>`:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
client.flag("beta", context: {user_id: "alice", country: "GB"})
|
|
87
|
+
client.flag("beta", context: {user: {id: "alice", plan: "premium"}})
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Send an identifier** (`user_id`, `unit_id`, or `user: {id:}`) whenever you
|
|
91
|
+
want a stable answer. Percentage rollouts and A/B variations hash it, so an
|
|
92
|
+
anonymous context re-rolls on every call by design.
|
|
93
|
+
|
|
94
|
+
Note that `flag` without a context serves from the cached `all_flags` payload;
|
|
95
|
+
with a context it asks for a fresh evaluation.
|
|
96
|
+
|
|
97
|
+
## Remote config
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
client.config("rate_limit", default: 100)
|
|
101
|
+
client.get_config("rate_limit") # full record, not just the value
|
|
102
|
+
client.list_configs
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## AI configs
|
|
106
|
+
|
|
107
|
+
Prompts, agents, skills and rules, versioned per environment and editable
|
|
108
|
+
without a deploy.
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
client.ai_config("support-agent.md")
|
|
112
|
+
client.list_ai_configs(file_type: "agent")
|
|
113
|
+
client.list_ai_configs(folder: "support") # :any for every folder
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Translations
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
client.translation("checkout.greeting", locale: "fr", default: "Hello",
|
|
120
|
+
variables: {name: "Alice"})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The key is `namespace.message`. `{placeholders}` come from `variables`, and the
|
|
124
|
+
default is returned whenever the catalogue, namespace or message is missing — a
|
|
125
|
+
lookup never raises.
|
|
126
|
+
|
|
127
|
+
## Experiments
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
assignment = client.experiment("checkout-redesign", context: {user_id: "alice"})
|
|
131
|
+
|
|
132
|
+
if assignment && assignment["variant"] == "treatment"
|
|
133
|
+
# ...
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
client.track_experiment_metric(
|
|
137
|
+
experiment_key: "checkout-redesign",
|
|
138
|
+
event_name: "purchase",
|
|
139
|
+
user_id: "alice",
|
|
140
|
+
value: 42.50,
|
|
141
|
+
properties: {currency: "GBP"}
|
|
142
|
+
)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`experiment` returns `nil` for a context with no identifier — an assignment
|
|
146
|
+
that cannot be stable is worse than none.
|
|
147
|
+
|
|
148
|
+
Metrics are buffered in memory and only touch the network on `flush` or
|
|
149
|
+
`close`:
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
client.flush # send now
|
|
153
|
+
client.close # flush and release the connection
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
In a long-running process call `flush` periodically; in a request/response
|
|
157
|
+
cycle, `close` at the end is enough.
|
|
158
|
+
|
|
159
|
+
## Caching
|
|
160
|
+
|
|
161
|
+
Reads are cached in memory for `cache_ttl` seconds (60 by default), so a burst
|
|
162
|
+
of `flag` calls costs one request.
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
client.clear_cache
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Failure behaviour
|
|
169
|
+
|
|
170
|
+
This SDK deliberately splits the two cases:
|
|
171
|
+
|
|
172
|
+
- **Evaluation reads return your default.** `flag`, `config` and `translation`
|
|
173
|
+
degrade to the fallback rather than raising, so an outage cannot take a
|
|
174
|
+
request path down with it.
|
|
175
|
+
- **Metadata lists raise.** `list_flags`, `list_configs` and `list_ai_configs`
|
|
176
|
+
are operational calls, and swallowing their failure would hide a bad key or a
|
|
177
|
+
wrong base URL behind an empty array.
|
|
178
|
+
|
|
179
|
+
## Security
|
|
180
|
+
|
|
181
|
+
Keep the server key out of anything you ship to a browser or a phone. Grant a
|
|
182
|
+
key only the read scopes it needs; a client key never receives targeting rules,
|
|
183
|
+
so the client cannot see who else you are targeting.
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT
|
data/lib/flagdash.rb
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "net/http"
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "time"
|
|
5
|
+
require "uri"
|
|
6
|
+
require_relative "flagdash/version"
|
|
7
|
+
|
|
8
|
+
module FlagDash
|
|
9
|
+
Detail = Struct.new(:key, :value, :reason, :variation_key, keyword_init: true)
|
|
10
|
+
|
|
11
|
+
class Client
|
|
12
|
+
REGION_ENV = %w[FLAGDASH_REGION FLY_REGION AWS_REGION AWS_DEFAULT_REGION VERCEL_REGION GOOGLE_CLOUD_REGION RAILWAY_REPLICA_REGION RENDER_REGION].freeze
|
|
13
|
+
|
|
14
|
+
def initialize(sdk_key:, base_url: "https://flagdash.io", timeout: 5, cache_ttl: 60, region: nil, transport: nil)
|
|
15
|
+
raise ArgumentError, "sdk_key is required" if sdk_key.to_s.empty?
|
|
16
|
+
|
|
17
|
+
@sdk_key = sdk_key
|
|
18
|
+
@base_url = base_url.delete_suffix("/")
|
|
19
|
+
@timeout = timeout
|
|
20
|
+
@cache_ttl = cache_ttl
|
|
21
|
+
@region = region || REGION_ENV.filter_map { |name| ENV[name] }.first
|
|
22
|
+
@transport = transport || method(:http_request)
|
|
23
|
+
@cache = {}
|
|
24
|
+
@mutex = Mutex.new
|
|
25
|
+
@events = []
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def flag(key, default: false, context: nil)
|
|
29
|
+
return flag_detail(key, default: default, context: context).value if context && !context.empty?
|
|
30
|
+
|
|
31
|
+
cached("flag:#{key}") { all_flags.fetch(key, default) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def flag_detail(key, default: nil, context: nil)
|
|
35
|
+
data = request(:get, "/server/flags/#{segment(key)}", params: context_params(context))
|
|
36
|
+
flag = data.fetch("flag")
|
|
37
|
+
Detail.new(key: flag.fetch("key", key), value: flag.fetch("evaluated_value", default),
|
|
38
|
+
reason: flag.fetch("evaluation_path", "default"), variation_key: flag["variation_key"])
|
|
39
|
+
rescue StandardError
|
|
40
|
+
Detail.new(key: key, value: default, reason: "default", variation_key: nil)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def all_flags(context: nil)
|
|
44
|
+
return fetch_flags(context) if context && !context.empty?
|
|
45
|
+
|
|
46
|
+
cached("all_flags") { fetch_flags(nil) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def list_flags
|
|
50
|
+
request(:get, "/server/flags", params: context_params(nil)).fetch("flags", [])
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def config(key, default: nil)
|
|
54
|
+
cached("config:#{key}") do
|
|
55
|
+
request(:get, "/server/configs/#{segment(key)}").fetch("config", {}).fetch("value", default)
|
|
56
|
+
end
|
|
57
|
+
rescue StandardError
|
|
58
|
+
default
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def get_config(key)
|
|
62
|
+
request(:get, "/server/configs/#{segment(key)}")["config"]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def list_configs
|
|
66
|
+
request(:get, "/server/configs").fetch("configs", [])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def ai_config(file_name)
|
|
70
|
+
cached("ai:#{file_name}") { request(:get, "/server/ai-configs/#{segment(file_name)}")["ai_config"] }
|
|
71
|
+
rescue StandardError
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def list_ai_configs(file_type: nil, folder: :any)
|
|
76
|
+
request(:get, "/server/ai-configs").fetch("ai_configs", []).select do |item|
|
|
77
|
+
(!file_type || item["file_type"] == file_type.to_s) && (folder == :any || item["folder"] == folder)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def translation(key, locale:, default: nil, variables: {})
|
|
82
|
+
namespace, message = key.split(".", 2)
|
|
83
|
+
return default || key unless message
|
|
84
|
+
|
|
85
|
+
catalog = cached("translation:#{locale}:#{namespace}") do
|
|
86
|
+
request(:get, "/server/translations/#{segment(locale)}/#{segment(namespace)}").fetch("catalog", {})
|
|
87
|
+
end
|
|
88
|
+
pattern = catalog.fetch("messages", {})[message]
|
|
89
|
+
return default || key unless pattern
|
|
90
|
+
|
|
91
|
+
pattern.gsub(/\{([\w.]+)\}/) { |match| variables.fetch(Regexp.last_match(1), variables.fetch(Regexp.last_match(1).to_sym, match)).to_s }
|
|
92
|
+
rescue StandardError
|
|
93
|
+
default || key
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def experiment(key, context:)
|
|
97
|
+
return nil unless identity(context)
|
|
98
|
+
|
|
99
|
+
request(:get, "/server/experiments/#{segment(key)}", params: context_params(context))["experiment"]
|
|
100
|
+
rescue StandardError
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def track_experiment_metric(experiment_key:, event_name:, user_id:, value: nil, properties: {}, event_id: nil, occurred_at: nil)
|
|
105
|
+
event = {event_id: event_id || "evt_#{SecureRandom.uuid}", experiment_key: experiment_key,
|
|
106
|
+
event_name: event_name, user_id: user_id, value: value, properties: properties,
|
|
107
|
+
occurred_at: occurred_at || Time.now.utc.iso8601}
|
|
108
|
+
@mutex.synchronize { @events << event if @events.length < 1_000 }
|
|
109
|
+
nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def flush
|
|
113
|
+
loop do
|
|
114
|
+
batch = @mutex.synchronize { @events.first(100) }
|
|
115
|
+
break if batch.empty?
|
|
116
|
+
|
|
117
|
+
request(:post, "/server/experiment-events/batch", json: {events: batch})
|
|
118
|
+
@mutex.synchronize { @events.shift(batch.length) }
|
|
119
|
+
end
|
|
120
|
+
true
|
|
121
|
+
rescue StandardError
|
|
122
|
+
false
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def clear_cache
|
|
126
|
+
@mutex.synchronize { @cache.clear }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def close
|
|
130
|
+
flush
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def fetch_flags(context)
|
|
136
|
+
values = request(:get, "/server/flags", params: context_params(context)).fetch("evaluated", {})
|
|
137
|
+
@mutex.synchronize { values.each { |key, value| put_cache("flag:#{key}", value) } } unless context
|
|
138
|
+
values
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def cached(key)
|
|
142
|
+
return yield if @cache_ttl.zero?
|
|
143
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
144
|
+
hit = @mutex.synchronize { @cache[key] }
|
|
145
|
+
return hit[0] if hit && hit[1] > now
|
|
146
|
+
|
|
147
|
+
value = yield
|
|
148
|
+
@mutex.synchronize { put_cache(key, value) }
|
|
149
|
+
value
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def put_cache(key, value)
|
|
153
|
+
@cache[key] = [value, Process.clock_gettime(Process::CLOCK_MONOTONIC) + @cache_ttl]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def request(method, path, params: {}, json: nil)
|
|
157
|
+
@transport.call(method, "#{@base_url}/api/v1#{path}", params, json, @sdk_key, @timeout)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def http_request(method, url, params, json, key, timeout)
|
|
161
|
+
uri = URI(url)
|
|
162
|
+
uri.query = URI.encode_www_form(params) unless params.empty?
|
|
163
|
+
request = method == :post ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
|
|
164
|
+
request["Authorization"] = "Bearer #{key}"
|
|
165
|
+
request["Content-Type"] = "application/json"
|
|
166
|
+
request.body = JSON.generate(json) if json
|
|
167
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: timeout, read_timeout: timeout) { |http| http.request(request) }
|
|
168
|
+
raise "FlagDash HTTP #{response.code}" unless response.is_a?(Net::HTTPSuccess)
|
|
169
|
+
|
|
170
|
+
JSON.parse(response.body)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def context_params(context)
|
|
174
|
+
result = (context || {}).transform_keys(&:to_s)
|
|
175
|
+
user = result.delete("user")
|
|
176
|
+
user&.each { |key, value| result[key.to_s == "id" ? "user_id" : "user_#{key}"] = value }
|
|
177
|
+
result["region"] ||= @region if @region
|
|
178
|
+
result
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def identity(context)
|
|
182
|
+
context[:user_id] || context["user_id"] || context[:unit_id] || context["unit_id"] || context.dig(:user, :id) || context.dig("user", "id")
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def segment(value)
|
|
186
|
+
URI.encode_www_form_component(value.to_s).gsub("+", "%20")
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: flagdash
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- FlagDash
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-08-25 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description:
|
|
14
|
+
email:
|
|
15
|
+
- support@flagdash.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- LICENSE
|
|
21
|
+
- README.md
|
|
22
|
+
- lib/flagdash.rb
|
|
23
|
+
- lib/flagdash/version.rb
|
|
24
|
+
homepage: https://flagdash.com/docs#sdk-ruby
|
|
25
|
+
licenses:
|
|
26
|
+
- MIT
|
|
27
|
+
metadata:
|
|
28
|
+
source_code_uri: https://github.com/flagdash/flagdash-ruby
|
|
29
|
+
changelog_uri: https://github.com/flagdash/flagdash-ruby/releases
|
|
30
|
+
post_install_message:
|
|
31
|
+
rdoc_options: []
|
|
32
|
+
require_paths:
|
|
33
|
+
- lib
|
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '3.1'
|
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '0'
|
|
44
|
+
requirements: []
|
|
45
|
+
rubygems_version: 3.5.22
|
|
46
|
+
signing_key:
|
|
47
|
+
specification_version: 4
|
|
48
|
+
summary: Official server-side FlagDash SDK for Ruby
|
|
49
|
+
test_files: []
|