///|
pub(all) suberror BuildError {
InvalidBuildConfig(String)
} derive(Eq, Debug)
///|
pub(all) struct TransformConfig {
name : String
args : Json
} derive(Eq, Debug)
///|
pub(all) struct ProjectionConfig {
name : String
is_abstract : Bool
imports : Array[String]
transforms : Array[TransformConfig]
plugins : Map[String, Json]
} derive(Eq, Debug)
///|
pub(all) struct BuildConfig {
version : String
sources : Array[String]
imports : Array[String]
output_directory : String
projections : Array[ProjectionConfig]
plugins : Map[String, Json]
ignore_missing_plugins : Bool
} derive(Eq, Debug)
///|
fn config_field(value : Json, key : String) -> Json? {
match value {
Object(fields) => fields.get(key)
_ => None
}
}
///|
fn config_string(value : Json, key : String) -> String? {
match config_field(value, key) {
Some(String(s)) => Some(s)
_ => None
}
}
///|
fn config_bool(value : Json, key : String) -> Bool raise BuildError {
match config_field(value, key) {
None => false
Some(True) => true
Some(False) => false
_ => raise InvalidBuildConfig("Expected `" + key + "` to be a boolean")
}
}
///|
fn config_optional_string(
value : Json,
key : String,
) -> String? raise BuildError {
match config_field(value, key) {
None => None
Some(String(text)) => Some(text)
_ => raise InvalidBuildConfig("Expected `" + key + "` to be a string")
}
}
///|
fn config_strings(value : Json, key : String) -> Array[String] raise BuildError {
match config_field(value, key) {
None => []
Some(Array(values)) => {
let result : Array[String] = []
for entry in values {
guard entry is String(text) else {
raise InvalidBuildConfig(
"Expected `" + key + "` to contain only strings",
)
}
result.push(text)
}
result
}
_ => raise InvalidBuildConfig("Expected `" + key + "` to be an array")
}
}
///|
fn config_object(
value : Json,
key : String,
) -> Map[String, Json] raise BuildError {
match config_field(value, key) {
None => Map([])
Some(Object(fields)) => fields
_ => raise InvalidBuildConfig("Expected `" + key + "` to be an object")
}
}
///|
fn config_plugins(value : Json) -> Map[String, Json] raise BuildError {
let plugins = config_object(value, "plugins")
for name, settings in plugins {
guard settings is Object(_) else {
raise InvalidBuildConfig(
"Expected settings for plugin `" + name + "` to be an object",
)
}
}
plugins
}
///|
fn config_validate_maven(document : Json) -> Unit raise BuildError {
let maven = Json::object(config_object(document, "maven"))
let dependencies = config_strings(maven, "dependencies")
match config_field(maven, "repositories") {
None => ()
Some(Array(repositories)) =>
for repository in repositories {
guard repository is Object(_) else {
raise InvalidBuildConfig("Expected a Maven repository object")
}
guard config_optional_string(repository, "url") is Some(_) else {
raise InvalidBuildConfig("Expected a Maven repository `url`")
}
for key in ["id", "proxyHost", "httpCredentials", "proxyCredentials"] {
let value = config_optional_string(repository, key)
if (key == "httpCredentials" || key == "proxyCredentials") &&
value is Some(credentials) {
guard credentials.find(":") is Some(position) &&
position > 0 &&
position < credentials.length() - 1 else {
raise InvalidBuildConfig(
"Expected Maven `" + key + "` in the format user:pass",
)
}
}
}
}
_ =>
raise InvalidBuildConfig("Expected Maven `repositories` to be an array")
}
if !dependencies.is_empty() {
raise InvalidBuildConfig(
"Maven dependencies are not supported; use local imports",
)
}
}
///|
fn strip_json_comments(source : String) -> String raise BuildError {
let chars = source.iter().to_array()
let out = StringBuilder()
let mut i = 0
let mut quoted = false
let mut escaped = false
while i < chars.length() {
let c = chars[i]
if quoted {
out.write_char(c)
if escaped {
escaped = false
} else if c == '\\' {
escaped = true
} else if c == '"' {
quoted = false
}
i += 1
} else if c == '"' {
quoted = true
out.write_char(c)
i += 1
} else if c == '/' && i + 1 < chars.length() && chars[i + 1] == '/' {
i += 2
while i < chars.length() && chars[i] != '\n' {
i += 1
}
} else if c == '/' && i + 1 < chars.length() && chars[i + 1] == '*' {
i += 2
let mut closed = false
while i + 1 < chars.length() {
if chars[i] == '*' && chars[i + 1] == '/' {
i += 2
closed = true
break
}
if chars[i] == '\n' {
out.write_char('\n')
}
i += 1
}
if !closed {
raise InvalidBuildConfig(
"Unterminated block comment in smithy-build.json",
)
}
} else {
out.write_char(c)
i += 1
}
}
out.to_string()
}
///|
fn config_expand_string(
text : String,
environment : Map[String, String],
) -> String raise BuildError {
let output = StringBuilder()
let mut copied = 0
let mut offset = 0
while offset < text.length() {
// Match the reference regex, which consumes the preceding character.
let start = if offset == 0 && text.has_prefix("${") {
0
} else if offset + 2 < text.length() &&
text[offset] != 92 &&
text[offset + 1] == 36 &&
text[offset + 2] == 123 {
offset + 1
} else {
offset += 1
continue
}
let variable_start = start + 2
guard text[variable_start:].find("}") is Some(end) && end > 0 else {
offset += 1
continue
}
let variable = text[variable_start:variable_start + end].to_owned()
guard environment.get(variable) is Some(replacement) else {
raise InvalidBuildConfig(
"Unable to expand variable `" +
variable +
"` to an environment variable",
)
}
output.write_string(text[copied:start].to_owned())
output.write_string(replacement)
offset = variable_start + end + 1
copied = offset
}
output.write_string(text[copied:].to_owned())
output
.to_string()
.split("\\$")
.map(part => part.to_owned())
.to_array()
.join("$")
}
///|
fn config_expand(
value : Json,
environment : Map[String, String],
) -> Json raise BuildError {
match value {
String(text) => Json::string(config_expand_string(text, environment))
Array(entries) =>
Json::array(entries.map(value => config_expand(value, environment)))
Object(fields) => {
let expanded : Map[String, Json] = Map([])
for key, value in fields {
expanded[config_expand_string(key, environment)] = config_expand(
value, environment,
)
}
Json::object(expanded)
}
_ => value
}
}
///|
pub fn BuildConfig::parse(
source : String,
environment~ : Map[String, String] = Map([]),
) -> BuildConfig raise BuildError {
let clean = strip_json_comments(source)
let document = @json.parse(clean) catch {
_ => raise InvalidBuildConfig("Invalid smithy-build.json")
}
let document = if source.contains("${") {
config_expand(document, environment)
} else {
document
}
guard document is Object(_) else {
raise InvalidBuildConfig("Expected a JSON object in smithy-build.json")
}
guard config_optional_string(document, "version") is Some(version) &&
(version == "1.0" || version == "2.0") else {
raise InvalidBuildConfig("Expected Smithy build version 1.0 or 2.0")
}
config_validate_maven(document)
let output_directory = config_optional_string(document, "outputDirectory").unwrap_or(
"build/smithy",
)
if output_directory.is_empty() {
raise InvalidBuildConfig("outputDirectory must be set to a valid directory")
}
let projections : Array[ProjectionConfig] = []
for name, value in config_object(document, "projections") {
guard value is Object(_) else {
raise InvalidBuildConfig(
"Expected projection `" + name + "` to be an object",
)
}
let transforms : Array[TransformConfig] = []
match config_field(value, "transforms") {
None => ()
Some(Array(entries)) =>
for entry in entries {
guard entry is Object(_) else {
raise InvalidBuildConfig(
"Expected a transform object in projection `" + name + "`",
)
}
guard config_optional_string(entry, "name") is Some(transform_name) else {
raise InvalidBuildConfig(
"Expected a transform name in projection `" + name + "`",
)
}
let args = config_field(entry, "args").unwrap_or(
Json::object(Map([])),
)
guard args is Object(_) || args is Array(_) else {
raise InvalidBuildConfig(
"Expected transform `args` to be an object or array",
)
}
transforms.push({ name: transform_name, args, })
}
_ =>
raise InvalidBuildConfig(
"Expected transforms array in projection `" + name + "`",
)
}
let is_abstract = config_bool(value, "abstract")
let imports = config_strings(value, "imports")
let plugins = config_plugins(value)
if is_abstract && (!imports.is_empty() || !plugins.is_empty()) {
raise InvalidBuildConfig(
"Abstract projections must not define plugins or imports",
)
}
projections.push({ name, is_abstract, imports, transforms, plugins, })
}
{
version,
sources: config_strings(document, "sources"),
imports: config_strings(document, "imports"),
output_directory,
projections,
plugins: config_plugins(document),
ignore_missing_plugins: config_bool(document, "ignoreMissingPlugins"),
}
}