Module: Quonfig::Datadir
- Defined in:
- lib/quonfig/datadir.rb
Overview
Loads a Quonfig workspace from the local filesystem (offline / datadir mode). Mirrors sdk-node/src/datadir.ts.
The workspace directory layout matches integration-test-data:
<datadir>/quonfig.json
<datadir>/configs/*.json
<datadir>/feature-flags/*.json
<datadir>/segments/*.json
<datadir>/log-levels/*.json
schemas/ is intentionally excluded — those files are raw JSON Schema documents, not Configs, and SDKs do not consume them (qfg-uzsl).
Each <type>/*.json file is a WorkspaceConfigDocument. The loader projects it down to the ConfigResponse shape that the SSE/HTTP delivery path emits, so ConfigStore consumes both transports uniformly.
Constant Summary collapse
- CONFIG_SUBDIRS =
%w[configs feature-flags segments log-levels].freeze
Class Method Summary collapse
- .coerce_numeric_value_field(hash) ⇒ Object
-
.coerce_numeric_values(node) ⇒ Object
Config files store int/double Value fields as JSON strings (‘“type”:“int”,“value”:“123”`).
- .effective_send_to_client_sdk(type, raw) ⇒ Object
-
.load_envelope(datadir, environment = nil) ⇒ Object
Read every config JSON in ‘datadir`, project to ConfigResponse hashes, and wrap in a ConfigEnvelope.
-
.load_store(datadir, environment = nil) ⇒ Object
Convenience: load the envelope and populate a fresh ConfigStore.
- .resolve_environment(quonfig_path, environment) ⇒ Object
- .to_config_response(raw, env_id) ⇒ Object
Class Method Details
.coerce_numeric_value_field(hash) ⇒ Object
122 123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/quonfig/datadir.rb', line 122 def coerce_numeric_value_field(hash) value = hash['value'] return unless value.is_a?(String) case hash['type'] when 'int' hash['value'] = Integer(value, 10) when 'double' hash['value'] = Float(value) end rescue ArgumentError, TypeError # Unparseable numeric string — leave the original value untouched. end |
.coerce_numeric_values(node) ⇒ Object
Config files store int/double Value fields as JSON strings (‘“type”:“int”,“value”:“123”`). api-delivery normalizes these to real numbers at config-load time (`Value.UnmarshalJSON`), so every envelope it emits over HTTP/SSE already carries JSON numbers. In datadir mode we read the files directly, so we must coerce here to match.
Walks the parsed config document in place, coercing every Value node — any Hash with a ‘type` of `“int”`/`“double”` and a String `value` — to a real number. A generic recursive walk covers `default.rules[].value`, environment rules, `criteria[].valueToMatch`, weighted-value arms, and variants without enumerating each location. On parse failure the original string is left in place (passthrough — never raise).
111 112 113 114 115 116 117 118 119 120 |
# File 'lib/quonfig/datadir.rb', line 111 def coerce_numeric_values(node) case node when Hash coerce_numeric_value_field(node) node.each_value { |child| coerce_numeric_values(child) } when Array node.each { |child| coerce_numeric_values(child) } end node end |
.effective_send_to_client_sdk(type, raw) ⇒ Object
93 94 95 96 97 |
# File 'lib/quonfig/datadir.rb', line 93 def effective_send_to_client_sdk(type, raw) return true if type == 'feature_flag' raw || false end |
.load_envelope(datadir, environment = nil) ⇒ Object
Read every config JSON in ‘datadir`, project to ConfigResponse hashes, and wrap in a ConfigEnvelope. Does no network I/O.
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/quonfig/datadir.rb', line 29 def load_envelope(datadir, environment = nil) env_id = resolve_environment(File.join(datadir, 'quonfig.json'), environment) configs = [] CONFIG_SUBDIRS.each do |subdir| dir = File.join(datadir, subdir) next unless Dir.exist?(dir) Dir.children(dir) .select { |name| name.end_with?('.json') } .sort .each do |filename| path = File.join(dir, filename) raw = JSON.parse(File.read(path)) raise ArgumentError, "[quonfig] config has empty key — file is not a Quonfig Config: #{path}" if raw['key'].nil? || raw['key'].to_s.empty? coerce_numeric_values(raw) configs << to_config_response(raw, env_id) end end Quonfig::ConfigEnvelope.new( configs: configs, meta: { 'version' => "datadir:#{datadir}", 'environment' => env_id } ) end |
.load_store(datadir, environment = nil) ⇒ Object
Convenience: load the envelope and populate a fresh ConfigStore.
57 58 59 60 61 62 |
# File 'lib/quonfig/datadir.rb', line 57 def load_store(datadir, environment = nil) envelope = load_envelope(datadir, environment) store = Quonfig::ConfigStore.new envelope.configs.each { |cfg| store.set(cfg['key'], cfg) } store end |
.resolve_environment(quonfig_path, environment) ⇒ Object
64 65 66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/quonfig/datadir.rb', line 64 def resolve_environment(quonfig_path, environment) environment ||= ENV.fetch('QUONFIG_ENVIRONMENT', nil) raise Quonfig::Errors::MissingEnvironmentError if environment.nil? || environment.empty? raise ArgumentError, "[quonfig] Datadir is missing quonfig.json: #{quonfig_path}" unless File.exist?(quonfig_path) environments = JSON.parse(File.read(quonfig_path)).fetch('environments', []) raise Quonfig::Errors::InvalidEnvironmentError.new(environment, environments) if !environments.empty? && !environments.include?(environment) environment end |
.to_config_response(raw, env_id) ⇒ Object
78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/quonfig/datadir.rb', line 78 def to_config_response(raw, env_id) environment = Array(raw['environments']).find { |e| e['id'] == env_id } type = raw['type'] { 'id' => raw['id'] || '', 'key' => raw['key'], 'type' => type, 'valueType' => raw['valueType'], 'sendToClientSdk' => effective_send_to_client_sdk(type, raw['sendToClientSdk']), 'default' => raw['default'] || { 'rules' => [] }, 'environment' => environment } end |