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

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