Module: Moult::Flags::Snapshot
- Defined in:
- lib/moult/flags/snapshot.rb
Overview
Ingests a LOCAL OpenFeature provider flag-state export and normalises it into one Moult-owned value object (FlagSet) the Staleness model can read. This is the flags analogue of Coverage (which ingests a SimpleCov/stdlib dump) and of Boundaries::Packwerk (which ingests packwerk's committed artifacts): an external format comes in, only Moult types go out, so the provider is swappable and nothing downstream depends on its on-disk shape.
One on-disk format is understood today (auto-detected, or forced via format:):
:flagd— a flagd flag-definition JSON, the OpenFeature-native provider- agnostic representation of flag state: {"flags" => {key => {"state" => "ENABLED"|"DISABLED", "variants" => {...}, "defaultVariant" => "...", "targeting" => {...}, "metadata" => {...}}}, "metadata" => {...}}.
flagd quirks normalised HERE and nowhere else (the swap point for a future provider/standard):
state"ENABLED"/"DISABLED" maps toenabledtrue/false.- A non-empty
targetingobject means the flag serves more than the default variant; an empty/absent one means it is fully rolled out to one variant. - flagd has no native archival/lifecycle state, so it is read from the standard
per-flag
metadataextension point:metadata.archived == true, ormetadata.lifecyclein "deprecated". - Timestamps (+metadata.updatedAt+/+lastModified+ per flag; the flag-set
metadataexport stamp) are captured as evidence only; the live, streaming provider connection that would make them authoritative is deferred, exactly like the live Coverband store.
This adapter takes NO dependency on the openfeature-sdk or any vendor SDK; it
reads the export with stdlib JSON, mirroring how Coverage needs no simplecov
and Boundaries::Packwerk no packwerk gem.
Defined Under Namespace
Classes: FlagSet, FlagState, Source
Constant Summary collapse
- ARCHIVED_LIFECYCLES =
%w[archived deprecated].freeze
Class Method Summary collapse
- .archived?(meta) ⇒ Boolean
-
.detect_format(raw) ⇒ Object
A flagd export is a JSON object with a top-level "flags" map whose every entry carries a "state" — the unambiguous discriminator.
-
.enabled?(state) ⇒ Boolean
ENABLED/DISABLED -> true/false; any other (or missing) state -> nil, which Staleness treats as neither rolled-out nor disabled (it falls through to active rather than inventing a verdict).
- .flag_state(key, defn) ⇒ Object
- .from_flagd(raw, path) ⇒ FlagSet
- .load(path, format: :auto) ⇒ FlagSet
-
.snapshot_timestamp(meta, path) ⇒ Object
Best-effort export stamp: an explicit flag-set metadata timestamp if present, else the file mtime (noted only as a fallback; it seeds deferred time-decay).
- .stringify(value) ⇒ Object
- .targeting?(targeting) ⇒ Boolean
Class Method Details
.archived?(meta) ⇒ Boolean
138 139 140 141 |
# File 'lib/moult/flags/snapshot.rb', line 138 def archived?() return true if ["archived"] == true ARCHIVED_LIFECYCLES.include?(["lifecycle"].to_s.downcase) end |
.detect_format(raw) ⇒ Object
A flagd export is a JSON object with a top-level "flags" map whose every entry carries a "state" — the unambiguous discriminator.
90 91 92 93 94 95 96 97 98 |
# File 'lib/moult/flags/snapshot.rb', line 90 def detect_format(raw) raise Moult::Error, "provider snapshot is not a JSON object" unless raw.is_a?(Hash) flags = raw["flags"] if flags.is_a?(Hash) && flags.values.all? { |f| f.is_a?(Hash) && f.key?("state") } :flagd else raise Moult::Error, "could not auto-detect provider snapshot format; pass --provider-format flagd" end end |
.enabled?(state) ⇒ Boolean
ENABLED/DISABLED -> true/false; any other (or missing) state -> nil, which Moult::Flags::Staleness treats as neither rolled-out nor disabled (it falls through to active rather than inventing a verdict).
131 132 133 134 135 136 |
# File 'lib/moult/flags/snapshot.rb', line 131 def enabled?(state) case state.to_s.upcase when "ENABLED" then true when "DISABLED" then false end end |
.flag_state(key, defn) ⇒ Object
115 116 117 118 119 120 121 122 123 124 125 126 |
# File 'lib/moult/flags/snapshot.rb', line 115 def flag_state(key, defn) defn = {} unless defn.is_a?(Hash) = defn["metadata"].is_a?(Hash) ? defn["metadata"] : {} FlagState.new( key: key, enabled: enabled?(defn["state"]), archived: archived?(), has_targeting: targeting?(defn["targeting"]), default_variant: defn["defaultVariant"], updated_at: stringify(["updatedAt"] || ["lastModified"]) ) end |
.from_flagd(raw, path) ⇒ FlagSet
101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/moult/flags/snapshot.rb', line 101 def from_flagd(raw, path) flags = raw["flags"].is_a?(Hash) ? raw["flags"] : {} = raw["metadata"].is_a?(Hash) ? raw["metadata"] : {} states = flags.each_with_object({}) do |(key, defn), acc| acc[key] = flag_state(key, defn) end source = Source.new( backend: "flagd", version: stringify(["version"]), exported_at: (, path) ) FlagSet.new(states: states, source: source) end |
.load(path, format: :auto) ⇒ FlagSet
75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/moult/flags/snapshot.rb', line 75 def load(path, format: :auto) raw = JSON.parse(File.read(path)) fmt = (format == :auto) ? detect_format(raw) : format case fmt when :flagd then from_flagd(raw, path) else raise Moult::Error, "unknown provider snapshot format: #{fmt}" end rescue JSON::ParserError => e raise Moult::Error, "could not parse provider snapshot #{path}: #{e.}" rescue Errno::ENOENT raise Moult::Error, "no such provider snapshot: #{path}" end |
.snapshot_timestamp(meta, path) ⇒ Object
Best-effort export stamp: an explicit flag-set metadata timestamp if present, else the file mtime (noted only as a fallback; it seeds deferred time-decay).
149 150 151 152 153 154 155 |
# File 'lib/moult/flags/snapshot.rb', line 149 def (, path) stamp = ["exportedAt"] || ["exported_at"] || ["updatedAt"] return stringify(stamp) if stamp File.mtime(path).utc.iso8601 rescue nil end |
.stringify(value) ⇒ Object
157 158 159 |
# File 'lib/moult/flags/snapshot.rb', line 157 def stringify(value) value&.to_s end |
.targeting?(targeting) ⇒ Boolean
143 144 145 |
# File 'lib/moult/flags/snapshot.rb', line 143 def targeting?(targeting) targeting.is_a?(Hash) && !targeting.empty? end |