///|
fn syntax_error(
code : String,
line : Int,
message : String,
expected : String,
actual : String,
) -> Diagnostic {
Diagnostic::new(
code,
"lines[" + line.to_string() + "]",
message,
expected,
actual,
)
}
///|
fn parse_key_values(
fields : Array[String],
start : Int,
line : Int,
) -> Result[Array[(String, String)], Diagnostic] {
let result : Array[(String, String)] = []
for index = start; index < fields.length(); index = index + 1 {
let field = fields[index]
let split = match field[:].find("=") {
Some(value) => value
None =>
return Err(
syntax_error(
"policy.option.syntax", line, "rule option is not key=value", "key=value",
field,
),
)
}
if split == 0 || split == field.length() - 1 {
return Err(
syntax_error(
"policy.option.syntax", line, "rule option has an empty key or value",
"key=value", field,
),
)
}
let key = field[:split].to_owned()
let value = field[split + 1:].to_owned()
for pair in result {
let (existing, _) = pair
if existing == key {
return Err(
syntax_error(
"policy.option.duplicate", line, "rule option appears more than once",
"unique option key", key,
),
)
}
}
result.push((key, value))
}
Ok(result)
}
///|
fn option_value(options : Array[(String, String)], key : String) -> String? {
for pair in options {
let (name, value) = pair
if name == key {
return Some(value)
}
}
None
}
///|
fn ensure_rule_options(
options : Array[(String, String)],
line : Int,
) -> Result[Unit, Diagnostic] {
let supported = [
"approvals", "checks", "labels", "forbid", "max_lines", "release_note", "binary",
]
for pair in options {
let (key, _) = pair
if !contains_string(supported, key) {
return Err(
syntax_error(
"policy.option.unknown",
line,
"rule option is unknown",
supported.join(","),
key,
),
)
}
}
Ok(())
}
///|
fn parse_forbidden(
value : String,
line : Int,
) -> Result[Array[ChangeKind], Diagnostic] {
let result : Array[ChangeKind] = []
for token in split_csv(value) {
match ChangeKind::parse(token) {
Some(kind) => if !contains_kind(result, kind) { result.push(kind) }
None =>
return Err(
syntax_error(
"policy.kind.invalid", line, "forbidden operation is invalid", "add, modify, delete, rename, or copy",
token,
),
)
}
}
Ok(result)
}
///|
fn parse_governance_rule(
fields : Array[String],
line : Int,
) -> Result[GovernanceRule, Diagnostic] {
if fields.length() < 3 {
return Err(
syntax_error(
"policy.rule.shape",
line,
"rule line has too few fields",
"RULE name glob [key=value ...]",
fields.join(" "),
),
)
}
let pattern = match PathGlob::compile(fields[2]) {
Ok(value) => value
Err(error) => return Err(error)
}
let options = match parse_key_values(fields, 3, line) {
Ok(value) => value
Err(error) => return Err(error)
}
match ensure_rule_options(options, line) {
Ok(_) => ()
Err(error) => return Err(error)
}
let approvals = match option_value(options, "approvals") {
Some(value) =>
match
parse_decimal(value, "lines[" + line.to_string() + "].approvals", 20) {
Ok(number) => number
Err(error) => return Err(error)
}
None => 0
}
let checks = match option_value(options, "checks") {
Some(value) => split_csv(value)
None => []
}
let labels = match option_value(options, "labels") {
Some(value) => split_csv(value)
None => []
}
let forbidden = match option_value(options, "forbid") {
Some(value) =>
match parse_forbidden(value, line) {
Ok(kinds) => kinds
Err(error) => return Err(error)
}
None => []
}
let max_lines = match option_value(options, "max_lines") {
Some("-") => None
Some(value) =>
match
parse_decimal(
value,
"lines[" + line.to_string() + "].max_lines",
10000000,
) {
Ok(number) => Some(number)
Err(error) => return Err(error)
}
None => None
}
let release_note = match option_value(options, "release_note") {
Some(value) =>
match
parse_yes_no(value, "lines[" + line.to_string() + "].release_note") {
Ok(flag) => flag
Err(error) => return Err(error)
}
None => false
}
let allow_binary = match option_value(options, "binary") {
Some("allow") => true
Some("deny") => false
Some(value) =>
return Err(
syntax_error(
"policy.binary.invalid", line, "binary option is invalid", "allow or deny",
value,
),
)
None => true
}
GovernanceRule::new(
fields[1],
pattern,
approvals~,
checks~,
labels~,
forbidden~,
max_lines~,
release_note~,
allow_binary~,
)
}
///|
pub fn parse_policy(source : String) -> Result[Policy, Diagnostic] {
if source.is_empty() || source.length() > 4194304 {
return Err(
Diagnostic::new(
"policy.input.limit",
"policy",
"policy is empty or exceeds the input limit",
"1 through 4194304 characters",
source.length().to_string(),
),
)
}
let lines = split_owned_lines(source)
if lines.is_empty() || lines[0] != "MOONCHANGE_POLICY 1" {
return Err(
syntax_error(
"policy.header",
0,
"policy header is missing or unsupported",
"MOONCHANGE_POLICY 1",
if lines.is_empty() {
"end of input"
} else {
lines[0]
},
),
)
}
let owners : Array[OwnerRule] = []
let rules : Array[GovernanceRule] = []
let mut default_approvals = 1
let mut max_total_lines : Int? = None
let mut require_owned = true
let mut saw_default = false
for index = 1; index < lines.length(); index = index + 1 {
let line = lines[index]
if line.is_empty() || line.has_prefix("#") {
continue
}
let fields = split_fields(line)
match fields[0] {
"DEFAULT" => {
if saw_default {
return Err(
syntax_error(
"policy.default.duplicate", index, "DEFAULT appears more than once",
"one DEFAULT line", line,
),
)
}
saw_default = true
let options = match parse_key_values(fields, 1, index) {
Ok(value) => value
Err(error) => return Err(error)
}
for pair in options {
let (key, _) = pair
if key != "approvals" && key != "max_total" && key != "require_owned" {
return Err(
syntax_error(
"policy.option.unknown", index, "default option is unknown", "approvals,max_total,require_owned",
key,
),
)
}
}
match option_value(options, "approvals") {
Some(value) =>
match
parse_decimal(
value,
"lines[" + index.to_string() + "].approvals",
20,
) {
Ok(number) => default_approvals = number
Err(error) => return Err(error)
}
None => ()
}
match option_value(options, "max_total") {
Some("-") => max_total_lines = None
Some(value) =>
match
parse_decimal(
value,
"lines[" + index.to_string() + "].max_total",
100000000,
) {
Ok(number) => max_total_lines = Some(number)
Err(error) => return Err(error)
}
None => ()
}
match option_value(options, "require_owned") {
Some(value) =>
match
parse_yes_no(
value,
"lines[" + index.to_string() + "].require_owned",
) {
Ok(flag) => require_owned = flag
Err(error) => return Err(error)
}
None => ()
}
}
"OWNER" => {
if fields.length() != 3 {
return Err(
syntax_error(
"policy.owner.shape", index, "owner line has the wrong shape", "OWNER glob @owner[,owner]",
line,
),
)
}
let pattern = match PathGlob::compile(fields[1]) {
Ok(value) => value
Err(error) => return Err(error)
}
let rule = match OwnerRule::new(pattern, split_csv(fields[2])) {
Ok(value) => value
Err(error) => return Err(error)
}
owners.push(rule)
}
"RULE" =>
match parse_governance_rule(fields, index) {
Ok(rule) => rules.push(rule)
Err(error) => return Err(error)
}
command =>
return Err(
syntax_error(
"policy.command.unknown", index, "policy command is unknown", "DEFAULT, OWNER, or RULE",
command,
),
)
}
}
Policy::new(
default_approvals~,
max_total_lines~,
require_owned~,
owner_rules=owners,
rules~,
)
}
///|
fn format_optional_csv(values : Array[String]) -> String {
if values.is_empty() {
"-"
} else {
values.join(",")
}
}
///|
pub fn Policy::to_text(self : Policy) -> String {
let output = StringBuilder()
let max_total = match self.max_total_lines {
Some(value) => value.to_string()
None => "-"
}
let require_owned = if self.require_owned { "yes" } else { "no" }
output.write_string("MOONCHANGE_POLICY 1\n")
output.write_string(
"DEFAULT approvals=" +
self.default_approvals.to_string() +
" max_total=" +
max_total +
" require_owned=" +
require_owned,
)
for owner in self.owner_rules {
output.write_string(
"\nOWNER " + owner.pattern.source() + " " + owner.owners.join(","),
)
}
for rule in self.rules {
let forbidden = rule.forbidden.map(fn(kind) { kind.to_text() })
let max_lines = match rule.max_lines {
Some(value) => value.to_string()
None => "-"
}
let release_note = if rule.release_note { "yes" } else { "no" }
let binary = if rule.allow_binary { "allow" } else { "deny" }
output.write_string(
"\nRULE " +
rule.name +
" " +
rule.pattern.source() +
" approvals=" +
rule.approvals.to_string() +
" checks=" +
format_optional_csv(rule.checks) +
" labels=" +
format_optional_csv(rule.labels) +
" forbid=" +
format_optional_csv(forbidden) +
" max_lines=" +
max_lines +
" release_note=" +
release_note +
" binary=" +
binary,
)
}
output.to_string()
}
///|
fn parse_change_line(
fields : Array[String],
line : Int,
) -> Result[Change, Diagnostic] {
if fields.length() < 6 {
return Err(
syntax_error(
"manifest.change.shape",
line,
"change line has too few fields",
"CHANGE kind path fields additions deletions text|binary",
fields.join(" "),
),
)
}
let kind = match ChangeKind::parse(fields[1]) {
Some(value) => value
None =>
return Err(
syntax_error(
"manifest.kind.invalid",
line,
"change operation is invalid",
"add, modify, delete, rename, or copy",
fields[1],
),
)
}
let path_fields = if kind == Rename || kind == Copy { 2 } else { 1 }
let expected_length = 2 + path_fields + 3
if fields.length() != expected_length {
return Err(
syntax_error(
"manifest.change.shape",
line,
"change line has the wrong number of fields",
if path_fields == 2 {
"CHANGE kind old new additions deletions text|binary"
} else {
"CHANGE kind path additions deletions text|binary"
},
fields.join(" "),
),
)
}
let additions_index = 2 + path_fields
let additions = match
parse_decimal(
fields[additions_index],
"lines[" + line.to_string() + "].additions",
10000000,
) {
Ok(value) => value
Err(error) => return Err(error)
}
let deletions = match
parse_decimal(
fields[additions_index + 1],
"lines[" + line.to_string() + "].deletions",
10000000,
) {
Ok(value) => value
Err(error) => return Err(error)
}
let binary = match fields[additions_index + 2] {
"text" => false
"binary" => true
value =>
return Err(
syntax_error(
"manifest.content.invalid", line, "content kind is invalid", "text or binary",
value,
),
)
}
let (old_path, new_path) = match kind {
Add => (None, Some(fields[2]))
Delete => (Some(fields[2]), None)
Modify => (Some(fields[2]), Some(fields[2]))
Rename | Copy => (Some(fields[2]), Some(fields[3]))
}
Change::new(kind, old_path, new_path, additions, deletions, binary~)
}
///|
pub fn parse_manifest(source : String) -> Result[ChangeSet, Diagnostic] {
if source.is_empty() || source.length() > 4194304 {
return Err(
Diagnostic::new(
"manifest.input.limit",
"manifest",
"manifest is empty or exceeds the input limit",
"1 through 4194304 characters",
source.length().to_string(),
),
)
}
let lines = split_owned_lines(source)
if lines.is_empty() || lines[0] != "MOONCHANGE 1" {
return Err(
syntax_error(
"manifest.header",
0,
"manifest header is missing or unsupported",
"MOONCHANGE 1",
if lines.is_empty() {
"end of input"
} else {
lines[0]
},
),
)
}
let mut id : String? = None
let mut actor : String? = None
let approvals : Array[String] = []
let checks : Array[CheckResult] = []
let labels : Array[String] = []
let changes : Array[Change] = []
let mut release_note = false
let mut saw_release_note = false
for index = 1; index < lines.length(); index = index + 1 {
let line = lines[index]
if line.is_empty() || line.has_prefix("#") {
continue
}
let fields = split_fields(line)
match fields[0] {
"ID" => {
if fields.length() != 2 || id is Some(_) {
return Err(
syntax_error(
"manifest.id.shape", index, "ID is duplicate or malformed", "one ID value",
line,
),
)
}
id = Some(fields[1])
}
"ACTOR" => {
if fields.length() != 2 || actor is Some(_) || !is_principal(fields[1]) {
return Err(
syntax_error(
"manifest.actor.shape", index, "ACTOR is duplicate, malformed, or invalid",
"one @principal", line,
),
)
}
actor = Some(fields[1])
}
"APPROVAL" => {
if fields.length() != 2 || !is_principal(fields[1]) {
return Err(
syntax_error(
"manifest.approval.shape", index, "APPROVAL is malformed", "APPROVAL @principal",
line,
),
)
}
push_unique_string(approvals, fields[1])
}
"CHECK" => {
if fields.length() != 3 || !is_simple_name(fields[1]) {
return Err(
syntax_error(
"manifest.check.shape", index, "CHECK is malformed", "CHECK name passed|failed|pending",
line,
),
)
}
let state = match CheckState::parse(fields[2]) {
Some(value) => value
None =>
return Err(
syntax_error(
"manifest.check.state",
index,
"check state is invalid",
"passed, failed, or pending",
fields[2],
),
)
}
for existing in checks {
if existing.name == fields[1] {
return Err(
syntax_error(
"manifest.check.duplicate",
index,
"check name appears more than once",
"unique check name",
fields[1],
),
)
}
}
checks.push(CheckResult::new(fields[1], state))
}
"LABEL" => {
if fields.length() != 2 || !is_simple_name(fields[1]) {
return Err(
syntax_error(
"manifest.label.shape", index, "LABEL is malformed", "LABEL name",
line,
),
)
}
push_unique_string(labels, fields[1])
}
"RELEASE_NOTE" => {
if fields.length() != 2 || saw_release_note {
return Err(
syntax_error(
"manifest.release_note.shape", index, "RELEASE_NOTE is duplicate or malformed",
"one RELEASE_NOTE yes|no", line,
),
)
}
saw_release_note = true
release_note = match
parse_yes_no(
fields[1],
"lines[" + index.to_string() + "].release_note",
) {
Ok(value) => value
Err(error) => return Err(error)
}
}
"CHANGE" =>
match parse_change_line(fields, index) {
Ok(change) => changes.push(change)
Err(error) => return Err(error)
}
command =>
return Err(
syntax_error(
"manifest.command.unknown", index, "manifest command is unknown", "ID, ACTOR, APPROVAL, CHECK, LABEL, RELEASE_NOTE, or CHANGE",
command,
),
)
}
}
if id is None {
return Err(
Diagnostic::new(
"manifest.id.missing", "manifest", "manifest has no ID", "ID value", "missing",
),
)
}
if actor is None {
return Err(
Diagnostic::new(
"manifest.actor.missing", "manifest", "manifest has no actor", "ACTOR @principal",
"missing",
),
)
}
approvals.sort_by(fn(left, right) { left.lexical_compare(right) })
labels.sort_by(fn(left, right) { left.lexical_compare(right) })
let evidence = Evidence::new(
actor.unwrap(),
approvals,
checks,
labels,
release_note~,
)
ChangeSet::new(id.unwrap(), changes, evidence)
}
///|
pub fn ChangeSet::to_text(self : ChangeSet) -> String {
let output = StringBuilder()
let release_note = if self.evidence.release_note { "yes" } else { "no" }
output.write_string(
"MOONCHANGE 1\nID " + self.id + "\nACTOR " + self.evidence.actor,
)
for approval in self.evidence.approvals {
output.write_string("\nAPPROVAL " + approval)
}
for check in self.evidence.checks {
output.write_string("\nCHECK " + check.name + " " + check.state.to_text())
}
for label in self.evidence.labels {
output.write_string("\nLABEL " + label)
}
output.write_string("\nRELEASE_NOTE " + release_note)
for change in self.changes {
output.write_string("\nCHANGE " + change.kind.to_text() + " ")
match change.kind {
Add => output.write_string(change.new_path.unwrap())
Delete => output.write_string(change.old_path.unwrap())
Modify => output.write_string(change.new_path.unwrap())
Rename | Copy =>
output.write_string(
change.old_path.unwrap() + " " + change.new_path.unwrap(),
)
}
let content_kind = if change.binary { " binary" } else { " text" }
output.write_string(
" " +
change.additions.to_string() +
" " +
change.deletions.to_string() +
content_kind,
)
}
output.to_string()
}