indie_flags 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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/indie_flags.rb +111 -0
  3. metadata +45 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 48ad1b3474f96cb021eade2c8f175d23c5c5a103bd016d94e5a499357df74252
4
+ data.tar.gz: 55b3114150143bac53540dd64033e105c45bdda897374cf27cd4b42614246044
5
+ SHA512:
6
+ metadata.gz: c6de3713329dfa1a23f9e74513731f497f674b06cd85519204ccb25924e9bd91ab6786796d8d0ae3d62ab6e4aee8a7dcb53fa8aeeb4d6b35b4ca793efc711dc2
7
+ data.tar.gz: 0c3bb58577fd781214bc44f82d0d9cb9b85438f570ad1d50e85b07cd40524838a6227f4fc1f0628ca38d326850f1f944378aeb7d07fd7f608620260a562f4012
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ module IndieFlags
8
+ DEFAULT_BASE_URL = "https://api.indieflags.dev"
9
+ DEFAULT_CACHE_TTL_SEC = 60
10
+
11
+ class Client
12
+ def initialize(api_key:, base_url: nil, env: "production", email: nil, cache_ttl_sec: DEFAULT_CACHE_TTL_SEC)
13
+ @api_key = api_key.to_s.strip
14
+ raise ArgumentError, "IndieFlags: api_key is required" if @api_key.empty?
15
+
16
+ @base_url = (base_url || DEFAULT_BASE_URL).to_s.chomp("/")
17
+ @env = (env || "production").to_s.strip.downcase
18
+ @env = "production" if @env.empty?
19
+ @email = email.to_s.strip.empty? ? nil : email.to_s.strip
20
+ @cache_ttl_sec = cache_ttl_sec
21
+ @cache = nil
22
+ @cache_fetched_at = 0
23
+ @mutex = Mutex.new
24
+ end
25
+
26
+ # Fetch all flags and cache. Call once at startup.
27
+ def init
28
+ flags = fetch_all_flags
29
+ if @cache_ttl_sec > 0
30
+ @mutex.synchronize do
31
+ @cache = flags
32
+ @cache_fetched_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
33
+ end
34
+ end
35
+ flags
36
+ end
37
+
38
+ def enabled?(flag_key)
39
+ get_value(flag_key) == true
40
+ end
41
+
42
+ def get_value(flag_key)
43
+ key = normalize_key(flag_key)
44
+ cached = get_from_cache
45
+ return cached[key] if cached && cached.key?(key)
46
+
47
+ single = fetch_single_flag(key)
48
+ return single unless single.nil?
49
+
50
+ false
51
+ end
52
+
53
+ def invalidate_cache
54
+ @mutex.synchronize { @cache = nil }
55
+ end
56
+
57
+ private
58
+
59
+ def normalize_key(raw)
60
+ raw.to_s.strip.downcase.tr(" ", "").tr("_", "-")
61
+ end
62
+
63
+ def get_from_cache
64
+ return nil if @cache_ttl_sec <= 0
65
+
66
+ @mutex.synchronize do
67
+ return nil if @cache.nil?
68
+ return nil if (Process.clock_gettime(Process::CLOCK_MONOTONIC) - @cache_fetched_at) > @cache_ttl_sec
69
+
70
+ @cache
71
+ end
72
+ end
73
+
74
+ def request(path, query = {})
75
+ uri = URI("#{@base_url}#{path}")
76
+ uri.query = URI.encode_www_form(query) unless query.empty?
77
+ req = Net::HTTP::Get.new(uri)
78
+ req["X-Api-Key"] = @api_key
79
+ req["X-Environment"] = @env
80
+ req["X-Flag-Email"] = @email if @email
81
+ res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }
82
+ return nil if res.code == "404"
83
+
84
+ raise "IndieFlags: #{res.code} #{res.body}" unless res.is_a?(Net::HTTPSuccess)
85
+
86
+ JSON.parse(res.body)
87
+ end
88
+
89
+ def fetch_all_flags
90
+ q = { "apiKey" => @api_key, "env" => @env }
91
+ q["email"] = @email if @email
92
+ data = request("/eval/flags", q)
93
+ return {} unless data
94
+ return {} unless data["flags"].is_a?(Hash)
95
+
96
+ data["flags"].transform_values do |v|
97
+ v.is_a?(String) ? v : !!v
98
+ end
99
+ end
100
+
101
+ def fetch_single_flag(flag_key)
102
+ q = { "apiKey" => @api_key, "flag" => flag_key, "env" => @env }
103
+ q["email"] = @email if @email
104
+ data = request("/eval", q)
105
+ return false unless data
106
+ return data["value"] if data.key?("value")
107
+
108
+ data["enabled"] == true
109
+ end
110
+ end
111
+ end
metadata ADDED
@@ -0,0 +1,45 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: indie_flags
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Minimalab
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-02-26 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: is_enabled, get_value, optional 60s cache. Indie Flags API.
14
+ email:
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/indie_flags.rb
20
+ homepage: https://indieflags.minimalab.ai
21
+ licenses:
22
+ - MIT
23
+ metadata:
24
+ source_code_uri: https://github.com/byminimalab/indieflags-api
25
+ documentation_uri: https://indieflags.minimalab.ai/docs
26
+ post_install_message:
27
+ rdoc_options: []
28
+ require_paths:
29
+ - lib
30
+ required_ruby_version: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '3.0'
35
+ required_rubygems_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ requirements: []
41
+ rubygems_version: 3.0.3.1
42
+ signing_key:
43
+ specification_version: 4
44
+ summary: Feature flags client for Indie Flags
45
+ test_files: []