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 to enabled true/false.
  • A non-empty targeting object 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 metadata extension point: metadata.archived == true, or metadata.lifecycle in "deprecated".
  • Timestamps (+metadata.updatedAt+/+lastModified+ per flag; the flag-set metadata export 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

Class Method Details

.archived?(meta) ⇒ Boolean

Returns:

  • (Boolean)


138
139
140
141
# File 'lib/moult/flags/snapshot.rb', line 138

def archived?(meta)
  return true if meta["archived"] == true
  ARCHIVED_LIFECYCLES.include?(meta["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.

Raises:



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).

Returns:

  • (Boolean)


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)
  meta = defn["metadata"].is_a?(Hash) ? defn["metadata"] : {}
  FlagState.new(
    key: key,
    enabled: enabled?(defn["state"]),
    archived: archived?(meta),
    has_targeting: targeting?(defn["targeting"]),
    default_variant: defn["defaultVariant"],
    updated_at: stringify(meta["updatedAt"] || meta["lastModified"])
  )
end

.from_flagd(raw, path) ⇒ FlagSet

Returns:



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"] : {}
  meta = 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(meta["version"]),
    exported_at: snapshot_timestamp(meta, path)
  )
  FlagSet.new(states: states, source: source)
end

.load(path, format: :auto) ⇒ FlagSet

Parameters:

  • path (String)

    path to the provider snapshot file

  • format (Symbol) (defaults to: :auto)

    :auto (default) or :flagd

Returns:



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.message}"
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 snapshot_timestamp(meta, path)
  stamp = meta["exportedAt"] || meta["exported_at"] || meta["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

Returns:

  • (Boolean)


143
144
145
# File 'lib/moult/flags/snapshot.rb', line 143

def targeting?(targeting)
  targeting.is_a?(Hash) && !targeting.empty?
end