///|
pub(all) enum Value {
Text(String)
Bare(String)
Number(String)
Boolean(Bool)
Null
List(Array[Value])
Reference(String)
PathReference(Array[String], Bool)
Object(Map[String, Value])
SealedObject(Map[String, Value])
Concat(Array[(String, Value)])
DelayedMerge(Value, Value)
Bound(Array[String], Int, Value)
Substitution(Array[Array[String]], Bool, Bool, Int)
} derive(Debug, Eq)
///|
pub(all) struct IncludeRequest {
name : String
kind : String
from : String
required : Bool
} derive(Debug, Eq, ToJson)
///|
pub(all) struct IncludeSource {
name : String
content : String
format : String
} derive(Debug, Eq)
///|
priv struct Work {
mut remaining : Int
mut next_id : Int
}
///|
fn Work::spend(self : Work, n : Int) -> Unit raise ParseError {
self.remaining -= n
if self.remaining < 0 {
raise Invalid("configuration expansion limit")
}
}
///|
fn Work::id(self : Work) -> Int {
self.next_id += 1
self.next_id
}
///|
priv struct ParseContext {
sources : Map[String, String]
loader : ((IncludeRequest) -> Result[Array[IncludeSource], String])?
work : Work
}
///|
fn uncertain(v : Value) -> Bool {
match v {
Reference(_)
| PathReference(_, _)
| Substitution(_, _, _, _)
| Bound(_, _, _)
| Concat(_)
| DelayedMerge(_, _) => true
_ => false
}
}
///|
fn merge(a : Value, b : Value) -> Value {
if ignores_fallbacks(b) {
return b
}
if uncertain(a) || uncertain(b) {
return append_delayed(a, b)
}
match (a, b) {
(Object(x) | SealedObject(x), Object(y)) => {
let combined = x.copy()
for k, v in y {
combined[k] = match combined.get(k) {
Some(old) => merge(old, v)
None => v
}
}
if !has_index_aliases(combined) {
return rebuild_object(a, combined)
}
let keys = Map([])
for k in object_iteration_keys(y) {
keys[k] = Null
}
for k in object_iteration_keys(x) {
keys[k] = Null
}
let out = Map([])
for k in object_iteration_keys(keys) {
out[k] = combined[k]
}
rebuild_object(a, out)
}
(_, Object(fields)) =>
if completely_resolved(b) {
SealedObject(fields)
} else {
DelayedMerge(a, b)
}
_ => DelayedMerge(a, b)
}
}
///|
fn install(
m : Map[String, Value],
keys : Array[String],
index : Int,
v : Value,
) -> Unit {
let k = keys[index]
let patch = if index == keys.length() - 1 {
v
} else {
let child : Map[String, Value] = Map([])
install(child, keys, index + 1, v)
Object(child)
}
m[k] = match m.get(k) {
Some(old) => merge(old, patch)
None => patch
}
}
///|
fn append_path(a : Array[String], b : Array[String]) -> Array[String] {
let out = a.copy()
for s in b {
out.push(s)
}
out
}
///|
fn skip_lines(c : Cursor) -> Unit {
while c.eat("\n") {
}
}
///|
fn path(
c : Cursor,
substitution? : Bool = false,
) -> Array[String] raise ParseError {
let parts : Array[String] = []
let mut current = ""
let mut present = false
let mut first = true
while !c.done() {
let token = c.tokens[c.pos]
if !token.quoted &&
["\n", "=", ":", "+=", "{", "}", ",", "[", "]", "$", "?"].contains(
token.text,
) {
break
}
c.pos += 1
if !first {
current += token.gap
if !token.gap.is_empty() {
present = true
}
}
first = false
if token.quoted {
current += token.text
present = true
} else {
for ch in token.text {
if ch == '.' {
if !present {
raise Located("empty path component must be quoted", token.position)
}
parts.push(current)
current = ""
present = false
} else {
current += ch.to_string()
present = true
}
}
}
}
if !present {
raise Located("path component required", c.location())
}
parts.push(current)
if parts.length() > 32 {
raise Invalid("path depth limit")
}
if !substitution && parts[0].is_empty() {
()
}
parts
}
///|
/// Parse HOCON path expressions, including quoted periods and empty components.
pub fn split_path(text : String) -> Array[String] raise ParseError {
if plain_ascii_path(text) {
return text.split(".").map(part => part.to_owned()).collect()
}
if trimmed_api_path(text) is Some(parts) {
return parts
}
let c = lex(text, "", comments=false)
let result = path(c, substitution=true)
if !c.done() {
raise Located("trailing path input", c.location())
}
result
}
///|
// Avoid token/source-position allocation for ordinary dotted ASCII paths.
// Every other spelling goes through the existing lexer, including malformed
// separators, quoting, comments, Unicode, resource limits and their diagnostics.
fn plain_ascii_path(text : String, outer_trimmed? : Bool = false) -> Bool {
if text.is_empty() || text.length() > 100000 {
return false
}
let mut component = false
let mut depth = 1
for unit in text.code_units() {
let n = unit.to_int()
if n == 46 {
if !component || depth == 32 {
return false
}
component = false
depth += 1
} else if (n >= 65 && n <= 90) ||
(n >= 97 && n <= 122) ||
(!outer_trimmed && n >= 48 && n <= 57) ||
(n == 45 && (!outer_trimmed || component)) ||
n == 95 {
component = true
} else {
return false
}
}
component
}
///|
// The native API accepts outer newlines only for its simple, nonnumeric path
// grammar. Quoted/numeric/Unicode spellings must retain full lexer behavior.
fn trimmed_api_path(text : String) -> Array[String]? {
if text.is_empty() || text.length() > 100000 {
return None
}
let units = text.code_units()
let mut start = 0
let mut end = units.length()
while start < end && whitespace(units[start].to_int().unsafe_to_char()) {
start += 1
}
while end > start && whitespace(units[end - 1].to_int().unsafe_to_char()) {
end -= 1
}
if start == 0 && end == units.length() {
return None
}
let trimmed = text.view(start_offset=start, end_offset=end).to_owned()
if plain_ascii_path(trimmed, outer_trimmed=true) {
Some(trimmed.split(".").map(part => part.to_owned()).collect())
} else {
None
}
}
///|
fn include_request(c : Cursor) -> IncludeRequest raise ParseError {
skip_lines(c)
let required = c.eat("required")
if required {
skip_lines(c)
c.need("(")
skip_lines(c)
}
let kind = if ["file", "url", "classpath"].contains(c.peek()) {
c.take().text
} else {
"heuristic"
}
if kind != "heuristic" {
skip_lines(c)
c.need("(")
skip_lines(c)
}
let token = c.take()
if !token.quoted {
raise Located("include requires one quoted resource name", token.position)
}
if kind != "heuristic" {
skip_lines(c)
c.need(")")
}
if required {
skip_lines(c)
c.need(")")
}
{ name: token.text, kind, from: token.position.source, required, }
}
///|
fn parse_source(
source : IncludeSource,
context : ParseContext,
active : Array[String],
prefix : Array[String],
scope : Array[String],
depth : Int,
) -> Value raise ParseError {
if source.content.length() > 100000 || !valid_unicode(source.content) {
raise Invalid("excessive or ill-formed source: " + source.name)
}
context.work.spend(source.content.length())
if source.format == "json" {
let mut nesting = depth
let mut quoted = false
let mut escaped = false
for c in source.content {
if quoted {
if escaped {
escaped = false
} else if c == '\\' {
escaped = true
} else if c == '"' {
quoted = false
}
} else if c == '"' {
quoted = true
} else if c == '[' || c == '{' {
nesting += 1
if nesting > 32 {
raise Invalid("JSON source nesting limit: " + source.name)
}
} else if c == ']' || c == '}' {
nesting -= 1
}
}
return configuration_json(source.content, source.name)
}
if source.format != "hocon" {
raise Invalid("unsupported source format: " + source.format)
}
let c = lex(source.content, source.name)
skip_lines(c)
let result = if c.peek() == "[" {
value(c, context, active, prefix, scope, depth)
} else {
let brace = c.eat("{")
object(c, context, active, prefix, scope, depth, brace)
}
skip_lines(c)
if !c.done() {
raise Located("trailing input", c.location())
}
result
}
///|
fn object(
c : Cursor,
context : ParseContext,
active : Array[String],
prefix : Array[String],
scope : Array[String],
depth : Int,
closing : Bool,
) -> Value raise ParseError {
if depth > 32 {
raise Invalid("object/include depth limit")
}
let out : Map[String, Value] = Map([])
skip_lines(c)
while !c.done() && c.peek() != "}" {
context.work.spend(1)
if c.eat("include") {
let req = include_request(c)
let sources = match context.loader {
Some(load) =>
match load(req) {
Ok(sources) => sources
Err(message) => raise Invalid(message)
}
None =>
match context.sources.get(req.name) {
Some(content) => [{ name: req.name, content, format: "hocon", }]
None => []
}
}
if sources.is_empty() && req.required {
raise Invalid("missing required include " + req.name)
}
for source in sources {
if active.contains(source.name) {
raise Invalid("include cycle: " + source.name)
}
let next = active.copy()
next.push(source.name)
let inc = parse_source(source, context, next, prefix, prefix, depth + 1)
match inc {
Object(fields) | SealedObject(fields) =>
for k, v in fields {
out[k] = match out.get(k) {
Some(old) => merge(old, v)
None => v
}
}
_ => raise Invalid("include must contain an object")
}
}
} else {
let keys = path(c)
let owner = append_path(prefix, keys)
let plus = c.eat("+=")
if !plus && c.peek() != "{" && !c.eat("=") && !c.eat(":") {
raise Located("missing assignment", c.location())
}
skip_lines(c)
let raw = value(c, context, active, owner, scope, depth + 1)
let rhs = if plus {
Concat([
("", Substitution([owner], true, false, context.work.id())),
("", List([raw])),
])
} else {
raw
}
let rhs = if uncertain(rhs) {
Bound(owner, context.work.id(), rhs)
} else {
rhs
}
install(out, keys, 0, rhs)
}
if c.eat(",") {
skip_lines(c)
} else if c.eat("\n") {
skip_lines(c)
} else if !c.done() && c.peek() != "}" {
raise Located("entries require newline or comma", c.location())
}
if c.peek() == "," {
raise Located("duplicate comma", c.location())
}
}
if closing {
c.need("}")
}
Object(out)
}
///|
fn value_end(c : Cursor) -> Bool {
c.done() || ["\n", ",", "}", "]"].contains(c.peek())
}
///|
fn atom(
c : Cursor,
context : ParseContext,
active : Array[String],
prefix : Array[String],
scope : Array[String],
depth : Int,
) -> Value raise ParseError {
if depth > 32 {
raise Invalid("value nesting limit")
}
context.work.spend(1)
if c.eat("{") {
return object(c, context, active, prefix, scope, depth + 1, true)
}
if c.eat("[") {
let items = []
skip_lines(c)
while !c.eat("]") {
items.push(value(c, context, active, prefix, scope, depth + 1))
if c.eat(",") {
skip_lines(c)
} else if c.eat("\n") {
skip_lines(c)
} else if c.peek() != "]" {
raise Located("array elements require newline or comma", c.location())
}
}
return List(items)
}
if c.eat("$") {
if c.done() || !c.tokens[c.pos].gap.is_empty() {
raise Located("substitution requires adjacent ${", c.location())
}
c.need("{")
let optional = if c.peek() == "?" {
if !c.tokens[c.pos].gap.is_empty() {
raise Located("optional marker must be adjacent", c.location())
}
c.pos += 1
true
} else {
false
}
let keys = path(c, substitution=true)
let list_env = c.eat("[")
if list_env {
c.need("]")
}
c.need("}")
let paths = if scope.is_empty() {
[keys]
} else {
[append_path(scope, keys), keys]
}
return Substitution(paths, optional, list_env, context.work.id())
}
let token = c.take()
if token.quoted {
return Text(token.text)
}
if ["=", ":", "+=", "[", "]", "{", "}", "\n", ",", "?"].contains(token.text) {
raise Located("value required", token.position)
}
if token.kind == "number" {
return Number(token.text)
}
if token.kind == "boolean" {
return Boolean(token.text == "true")
}
if token.kind == "null" {
return Null
}
let mut text = token.text
while !c.done() &&
!value_end(c) &&
c.tokens[c.pos].gap.is_empty() &&
!c.tokens[c.pos].quoted &&
!["number", "boolean", "null"].contains(c.tokens[c.pos].kind) &&
!["$", "[", "{", "=", ":", "+=", "?"].contains(c.peek()) {
text += c.take().text
}
Bare(text)
}
///|
fn value(
c : Cursor,
context : ParseContext,
active : Array[String],
prefix : Array[String],
scope : Array[String],
depth : Int,
) -> Value raise ParseError {
let parts = [("", atom(c, context, active, prefix, scope, depth))]
while !value_end(c) {
let gap = c.tokens[c.pos].gap
parts.push((gap, atom(c, context, active, prefix, scope, depth)))
}
simplify_concat(parts, context.work)
}
///|
fn from_json(j : Json, numbers : JsonNumbers) -> Value raise ParseError {
match j {
String(s) => Text(s)
Number(_, ..) => {
let text = numbers.values
.get(numbers.offset)
.unwrap_or_else(() => raise Invalid("JSON number spelling mismatch"))
numbers.offset += 1
Number(text)
}
True => Boolean(true)
False => Boolean(false)
Null => Null
Array(xs) => List(xs.map(value => from_json(value, numbers)))
Object(m) => {
let out = Map([])
for k, v in m {
out[k] = from_json(v, numbers)
}
Object(out)
}
}
}
///|
fn lookup_parts(root : Value, keys : Array[String]) -> Value? {
let mut current = root
for key in keys {
match current {
Object(m) | SealedObject(m) =>
match m.get(key) {
Some(v) => current = v
None => return None
}
_ => return None
}
}
Some(current)
}
///|
pub fn get(config : Value, key : String) -> Value? {
let keys = split_path(key) catch { _ => return None }
lookup_parts(config, keys)
}
///|
pub fn get_path(config : Value, keys : Array[String]) -> Value? {
lookup_parts(config, keys)
}
///|
pub fn parse(
source : String,
includes? : Map[String, String] = Map([]),
loader? : ((IncludeRequest) -> Result[Array[IncludeSource], String])? = None,
source_name? : String = "",
fallbacks? : Array[String] = [],
environment? : Map[String, String] = Map([]),
) -> Value raise ParseError {
parse_sources(
{ name: source_name, content: source, format: "hocon", },
includes~,
loader~,
fallbacks=fallbacks.mapi((i, content) => {
name: "",
content,
format: "hocon",
}),
environment~,
)
}
///|
/// Load ordered sources with explicit origins and a synchronous include host.
pub fn parse_sources(
source : IncludeSource,
includes? : Map[String, String] = Map([]),
loader? : ((IncludeRequest) -> Result[Array[IncludeSource], String])? = None,
fallbacks? : Array[IncludeSource] = [],
environment? : Map[String, String] = Map([]),
object_only? : Bool = false,
) -> Value raise ParseError {
resolve_document(
parse_sources_unresolved(
source,
includes~,
loader~,
fallbacks~,
object_only~,
),
environment,
)
}
///|
/// Parse and merge sources without resolving substitutions.
pub fn parse_sources_unresolved(
source : IncludeSource,
includes? : Map[String, String] = Map([]),
loader? : ((IncludeRequest) -> Result[Array[IncludeSource], String])? = None,
fallbacks? : Array[IncludeSource] = [],
object_only? : Bool = false,
) -> Value raise ParseError {
let context : ParseContext = {
sources: includes,
loader,
work: { remaining: 1000000, next_id: 0, },
}
let layers = [parse_source(source, context, [source.name], [], [], 0)]
for source in fallbacks {
layers.push(parse_source(source, context, [source.name], [], [], 0))
}
if object_only {
for layer in layers {
if !(layer is (Object(_) | SealedObject(_))) {
raise Invalid("configuration source root must be an object")
}
}
}
let mut root = layers[layers.length() - 1]
for i = layers.length() - 2; i >= 0; i = i - 1 {
root = merge(root, layers[i])
}
root
}
///|
pub fn parse_unresolved(
source : String,
includes? : Map[String, String] = Map([]),
loader? : ((IncludeRequest) -> Result[Array[IncludeSource], String])? = None,
source_name? : String = "",
fallbacks? : Array[String] = [],
) -> Value raise ParseError {
parse_sources_unresolved(
{ name: source_name, content: source, format: "hocon", },
includes~,
loader~,
fallbacks=fallbacks.mapi((i, content) => {
name: "",
content,
format: "hocon",
}),
)
}
///|
fn simplify_concat(
parts : Array[(String, Value)],
work : Work,
) -> Value raise ParseError {
let result : Array[Value] = []
let add = fn(v : Value) -> Unit raise ParseError {
if !result.is_empty() &&
!uncertain(result[result.length() - 1]) &&
!uncertain(v) {
let a = result[result.length() - 1]
result[result.length() - 1] = match (a, v) {
(Bare(space), Object(_) | SealedObject(_)) | (Bare(space), List(_)) =>
if space.iter().all(whitespace) {
v
} else {
raise Invalid("incompatible value concatenation")
}
(Object(_) | SealedObject(_), Bare(_)) | (List(_), Bare(_)) => a
(Object(_) | SealedObject(_), Object(_) | SealedObject(_)) =>
merge(a, v)
(List(xs), List(ys)) => {
let out = xs.copy()
for y in ys {
out.push(y)
}
work.spend(out.length())
List(out)
}
(Object(_) | SealedObject(_), _)
| (List(_), _)
| (_, Object(_) | SealedObject(_))
| (_, List(_)) => raise Invalid("incompatible value concatenation")
_ => {
let text = scalar_text(a) + scalar_text(v)
work.spend(text.length())
Text(text)
}
}
} else {
result.push(v)
}
}
for (gap, v) in parts {
if !gap.is_empty() {
add(Bare(gap))
}
add(v)
}
if result.length() == 1 {
result[0]
} else {
Concat(result.map(v => ("", v)))
}
}