///|
/// Legacy local discovery or explicitly selected resources in a local container.
pub(all) enum BuildInput {
Npm(@resources.NpmArchive, @resources.Selection)
Maven(@resources.MavenArchive, @resources.Selection?)
GitHub(@resources.GitHubArchive, @resources.Selection, @resources.HttpUrl)
Remote(@resources.PublicArchive, @resources.Selection)
Local(@resources.RepositoryLocation)
Selected(@resources.RepositoryLocation, @resources.Selection)
} derive(Eq, Debug)
///|
pub fn BuildInput::from_path(path : String) -> BuildInput raise BuildError {
guard @resources.RepositoryLocation::parse(path) is Some(location) else {
raise InvalidBuildConfig("Expected a nonempty local input location")
}
Local(location)
}
///|
pub fn BuildInput::location(self : BuildInput) -> @resources.RepositoryLocation {
match self {
Npm(archive, _) =>
@resources.RepositoryLocation::parse(archive.registry.to_string()).unwrap()
Maven(archive, _) =>
@resources.RepositoryLocation::parse(archive.repository.to_string()).unwrap()
GitHub(archive, _, _) =>
@resources.RepositoryLocation::parse(archive.repository.to_string()).unwrap()
Remote(archive, _) =>
@resources.RepositoryLocation::parse(archive.url.to_string()).unwrap()
Local(location) | Selected(location, _) => location
}
}
///|
pub fn BuildInput::with_location(
self : BuildInput,
location : @resources.RepositoryLocation,
) -> BuildInput raise BuildError {
match self {
Npm(archive, selection) => {
guard @resources.RepositoryUrl::parse(location.to_string())
is Some(registry) else {
raise InvalidBuildConfig("Expected an npm registry base URL")
}
Npm({ ..archive, registry, }, selection)
}
Maven(archive, selection) => {
guard @resources.RepositoryUrl::parse(location.to_string())
is Some(repository) else {
raise InvalidBuildConfig("Expected a Maven repository base URL")
}
Maven({ ..archive, repository, }, selection)
}
GitHub(archive, selection, api) => {
guard @resources.GitHubRepository::parse(location.to_string())
is Some(repository) else {
raise InvalidBuildConfig("Expected GitHub repository owner/name")
}
GitHub({ ..archive, repository, }, selection, api)
}
Remote(archive, selection) => {
guard @resources.HttpUrl::parse(location.to_string()) is Some(url) else {
raise InvalidBuildConfig(
"Expected an absolute credential-free HTTP(S) URL",
)
}
Remote({ ..archive, url, }, selection)
}
Local(_) => Local(location)
Selected(_, selection) => Selected(location, selection)
}
}
///|
pub fn BuildInput::to_json(self : BuildInput) -> Json {
match self {
Npm(archive, selection) => {
let json = Selected(self.location(), selection).to_json()
guard json is Object(fields) else { abort("selection serialization") }
ignore(fields.remove("path"))
fields["npm"] = {
"registry": archive.registry.to_string(),
"package": archive.reference.name.to_string(),
"version": archive.reference.version.to_string(),
}
if archive.expected_digest is Some(digest) {
fields["digest"] = Json::string(digest.to_string())
}
Json::object(fields)
}
Maven(archive, selection) => {
let fields = match selection {
Some(selection) => {
let json = Selected(self.location(), selection).to_json()
guard json is Object(fields) else { abort("selection serialization") }
ignore(fields.remove("path"))
fields
}
None => Map([])
}
let coordinates = archive.coordinates
let maven : Map[String, Json] = {
"repository": Json::string(archive.repository.to_string()),
"group": Json::string(coordinates.group.to_string()),
"artifact": Json::string(coordinates.artifact.to_string()),
"version": Json::string(coordinates.version.to_string()),
"checksum": Json::string(archive.checksum.algorithm().to_string()),
}
if coordinates.classifier is Some(classifier) {
maven["classifier"] = Json::string(classifier.to_string())
}
fields["maven"] = Json::object(maven)
if archive.expected_digest is Some(digest) {
fields["digest"] = Json::string(digest.to_string())
}
Json::object(fields)
}
GitHub(archive, selection, api) => {
let json = Remote(
{
url: api,
format: archive.format,
expected_digest: archive.expected_digest,
},
selection,
).to_json()
guard json is Object(fields) else { abort("selection serialization") }
ignore(fields.remove("url"))
fields["github"] = {
"repository": archive.repository.to_string(),
"tag": archive.reference.tag.to_string(),
"asset": archive.reference.asset.to_string(),
"api": api.to_string(),
}
Json::object(fields)
}
Remote(archive, selection) => {
let json = Selected(self.location(), selection).to_json()
guard json is Object(fields) else { abort("selection serialization") }
ignore(fields.remove("path"))
fields["url"] = Json::string(archive.url.to_string())
fields["format"] = Json::string(
match archive.format {
@resources.Zip => "zip"
@resources.Tar => "tar"
@resources.Tgz => "tgz"
},
)
if archive.expected_digest is Some(digest) {
fields["digest"] = Json::string(digest.to_string())
}
Json::object(fields)
}
Local(location) => Json::string(location.to_string())
Selected(location, selection) => {
let paths = []
let globs = []
for selector in selection.selectors() {
match selector {
@resources.Exact(path) => paths.push(Json::string(path.to_string()))
@resources.Named(path, name) =>
paths.push(
Json::object({
"path": Json::string(path.to_string()),
"name": Json::string(name.to_string()),
}),
)
@resources.Matching(pattern) =>
globs.push(Json::string(pattern.to_string()))
}
}
Json::object({
"path": Json::string(location.to_string()),
"root": Json::string(selection.root.to_string()),
"paths": Json::array(paths),
"globs": Json::array(globs),
})
}
}
}
///|
fn parse_entry_path(text : String) -> @resources.EntryPath raise BuildError {
@resources.EntryPath::parse(text).unwrap_or_else(() => {
raise InvalidBuildConfig("Unsafe resource path `" + text + "`")
})
}
///|
fn config_selection(value : Json) -> @resources.Selection raise BuildError {
guard config_optional_string(value, "root") is Some(root) &&
@resources.ResourceRoot::parse(root) is Some(root) else {
raise InvalidBuildConfig(
"Expected a safe resource `root`, using `.` for the container root",
)
}
let selectors = []
match config_field(value, "paths") {
None => ()
Some(Array(paths)) =>
for path in paths {
match path {
String(path) =>
selectors.push(@resources.Exact(parse_entry_path(path)))
Object(fields) => {
if fields.length() != 2 {
raise InvalidBuildConfig(
"A named resource path requires only `path` and `name`",
)
}
guard config_optional_string(path, "path") is Some(entry) &&
config_optional_string(path, "name") is Some(name) &&
@resources.ResourceName::parse(name) is Some(name) else {
raise InvalidBuildConfig(
"Expected a safe named resource `path` and `name`",
)
}
selectors.push(@resources.Named(parse_entry_path(entry), name))
}
_ =>
raise InvalidBuildConfig(
"Expected resource paths to be strings or named path objects",
)
}
}
_ => raise InvalidBuildConfig("Expected resource `paths` to be an array")
}
for glob in config_strings(value, "globs") {
guard @resources.GlobPattern::parse(glob) is Some(pattern) else {
raise InvalidBuildConfig("Invalid resource glob `" + glob + "`")
}
selectors.push(@resources.Matching(pattern))
}
guard @resources.Selection::new(root, selectors) is Some(selection) else {
raise InvalidBuildConfig(
"A resource selection requires between 1 and 256 paths or globs",
)
}
selection
}
///|
fn config_digest(value : Json) -> @resources.ContentDigest? raise BuildError {
match config_optional_string(value, "digest") {
None => None
Some(text) => {
guard @resources.ContentDigest::parse(text) is Some(digest) else {
raise InvalidBuildConfig(
"Expected digest sha256: followed by 64 hexadecimal digits",
)
}
Some(digest)
}
}
}
///|
fn config_repository_input(
value : Json,
provider : String,
) -> BuildInput raise BuildError {
guard value is Object(fields) &&
config_field(value, provider) is Some(Object(settings)) else {
raise InvalidBuildConfig("Expected a " + provider + " input object")
}
for key, _ in fields {
if ![provider, "digest", "root", "paths", "globs"].contains(key) {
raise InvalidBuildConfig(
"Unknown " + provider + " input field `" + key + "`",
)
}
}
let nested = Json::object(settings)
let allowed = if provider == "npm" {
["registry", "package", "version"]
} else {
["repository", "group", "artifact", "version", "classifier", "checksum"]
}
for key, _ in settings {
if !allowed.contains(key) {
raise InvalidBuildConfig(
"Unknown " + provider + " coordinate field `" + key + "`",
)
}
}
let expected_digest = config_digest(value)
if provider == "npm" {
guard @resources.RepositoryUrl::parse(
config_optional_string(nested, "registry").unwrap_or(
"https://registry.npmjs.org",
),
)
is Some(registry) &&
config_optional_string(nested, "package") is Some(name) &&
@resources.NpmPackageName::parse(name) is Some(name) &&
config_optional_string(nested, "version") is Some(version) &&
@resources.NpmVersion::parse(version) is Some(version) else {
raise InvalidBuildConfig(
"npm requires a public registry base, package name and exact semantic version",
)
}
Npm(
{ registry, reference: { name, version, }, expected_digest, },
config_selection(value),
)
} else {
guard @resources.RepositoryUrl::parse(
config_optional_string(nested, "repository").unwrap_or(
"https://repo.maven.apache.org/maven2",
),
)
is Some(repository) &&
config_optional_string(nested, "group") is Some(group) &&
@resources.MavenGroupId::parse(group) is Some(group) &&
config_optional_string(nested, "artifact") is Some(artifact) &&
@resources.MavenArtifactId::parse(artifact) is Some(artifact) &&
config_optional_string(nested, "version") is Some(version) &&
@resources.MavenVersion::parse(version) is Some(version) else {
raise InvalidBuildConfig(
"Maven requires a public repository base and exact group/artifact/version JAR coordinates",
)
}
let classifier = match config_optional_string(nested, "classifier") {
None => None
Some(text) => {
guard @resources.MavenClassifier::parse(text) is Some(value) else {
raise InvalidBuildConfig("Invalid Maven classifier")
}
Some(value)
}
}
let checksum = match
config_optional_string(nested, "checksum").unwrap_or("sha1") {
"sha1" => @resources.Sha1Sidecar
"sha256" => @resources.Sha256Sidecar
_ => raise InvalidBuildConfig("Expected Maven checksum sha1 or sha256")
}
let selection = if ["root", "paths", "globs"].any(key => {
fields.contains(key)
}) {
Some(config_selection(value))
} else {
None
}
Maven(
{
repository,
coordinates: { group, artifact, version, classifier, },
checksum,
expected_digest,
},
selection,
)
}
}
///|
fn config_input(value : Json) -> BuildInput raise BuildError {
if value is String(path) {
return BuildInput::from_path(path)
}
guard value is Object(fields) else {
raise InvalidBuildConfig(
"Expected a local input string or resource selection object",
)
}
if fields.contains("npm") {
return config_repository_input(value, "npm")
}
if fields.contains("maven") {
return config_repository_input(value, "maven")
}
let github = fields.contains("github")
let remote = fields.contains("url") || github
let allowed = if github {
["github", "format", "digest", "root", "paths", "globs"]
} else if remote {
["url", "format", "digest", "root", "paths", "globs"]
} else {
["path", "root", "paths", "globs"]
}
for key, _ in fields {
if !allowed.contains(key) {
raise InvalidBuildConfig("Unknown resource selection field `" + key + "`")
}
}
let location = if remote {
None
} else {
guard config_optional_string(value, "path") is Some(path) &&
@resources.RepositoryLocation::parse(path) is Some(location) else {
raise InvalidBuildConfig("Expected a resource selection `path`")
}
Some(location)
}
let selection = config_selection(value)
if remote {
let format = match
config_optional_string(value, "format").unwrap_or("zip") {
"zip" | "jar" => @resources.Zip
"tar" => @resources.Tar
"tgz" | "tar.gz" => @resources.Tgz
_ =>
raise InvalidBuildConfig(
"Expected URL archive format zip, jar, tar, tgz or tar.gz",
)
}
let expected_digest = config_digest(value)
if github {
guard config_field(value, "github") is Some(Object(github_fields)) else {
raise InvalidBuildConfig("Expected a github object")
}
for key, _ in github_fields {
if !["repository", "tag", "asset", "api"].contains(key) {
raise InvalidBuildConfig("Unknown GitHub field `" + key + "`")
}
}
let github_value = Json::object(github_fields)
guard config_optional_string(github_value, "repository")
is Some(repository) &&
@resources.GitHubRepository::parse(repository) is Some(repository) &&
config_optional_string(github_value, "tag") is Some(tag) &&
@resources.GitHubTag::parse(tag) is Some(tag) &&
config_optional_string(github_value, "asset") is Some(asset) &&
@resources.GitHubAssetName::parse(asset) is Some(asset) else {
raise InvalidBuildConfig(
"GitHub requires repository owner/name, an explicit tag and an exact asset name",
)
}
guard @resources.HttpUrl::parse(
config_optional_string(github_value, "api").unwrap_or(
"https://api.github.com",
),
)
is Some(api) &&
!api.to_string().contains("?") else {
raise InvalidBuildConfig(
"Expected a credential-free GitHub API base URL without query or fragment",
)
}
GitHub(
{ repository, reference: { tag, asset, }, format, expected_digest, },
selection,
api,
)
} else {
guard config_optional_string(value, "url") is Some(text) &&
@resources.HttpUrl::parse(text) is Some(url) else {
raise InvalidBuildConfig(
"Expected an absolute credential-free HTTP(S) URL without a fragment",
)
}
Remote({ url, format, expected_digest, }, selection)
}
} else {
Selected(location.unwrap(), selection)
}
}
///|
fn config_inputs(
value : Json,
key : String,
) -> Array[BuildInput] raise BuildError {
match config_field(value, key) {
None => []
Some(Array(values)) => values.map(config_input)
_ => raise InvalidBuildConfig("Expected `" + key + "` to be an array")
}
}
///|
/// Smithy accepts only selected model resources; generic selectors stay format-independent.
pub fn selected_models(
tree : @resources.EntryTree,
selection : @resources.Selection,
) -> Array[@resources.Resource] raise BuildError {
let loaded = @resources.select_resources(tree, selection) catch {
@resources.ResourceError(code, at, message) =>
raise InvalidBuildConfig(
"Could not load resource `" +
at.to_string() +
"` [" +
code.to_string() +
"]: " +
message,
)
}
for resource in loaded {
let path = resource.path.to_string()
let name = resource.name.to_string()
if (!path.has_suffix(".smithy") && !path.has_suffix(".json")) ||
path.has_suffix(".smithy") != name.has_suffix(".smithy") ||
path.has_suffix(".json") != name.has_suffix(".json") {
raise InvalidBuildConfig(
"Selected Smithy resource `" +
resource.provenance.to_string() +
"` must retain its .smithy or .json format in logical name `" +
name +
"`",
)
}
}
loaded
}