///|
pub(all) struct ChunkPolicy {
context_window : Int
separator : String
config : RedactionConfig
} derive(Debug, Eq)
///|
pub fn ChunkPolicy::default() -> ChunkPolicy {
{ context_window: 32, separator: "", config: RedactionConfig::default() }
}
///|
pub(all) struct ChunkResult {
chunk_id : String
original_length : Int
redacted_length : Int
result : DeidResult
} derive(Debug)
///|
pub(all) struct StreamingRedactor {
config : RedactionConfig
context_window : Int
mut carry : String
mut closed : Bool
} derive(Debug)
///|
pub fn StreamingRedactor::new(
config : RedactionConfig,
context_window : Int,
) -> StreamingRedactor {
{
config,
context_window: if context_window < 0 {
0
} else {
context_window
},
carry: "",
closed: false,
}
}
///|
fn stable_prefix(text : String, context_window : Int) -> (String, String) {
let keep = if context_window < 0 { 0 } else { context_window }
if text.length() <= keep {
("", text)
} else {
let split = text.length() - keep
(text[:split].to_owned(), text[split:].to_owned())
}
}
///|
pub fn StreamingRedactor::push(
self : StreamingRedactor,
chunk : String,
) -> String raise DeidError {
if self.closed {
raise DeidError("stream is already closed")
}
let combined = self.carry + chunk
let (stable, carry) = stable_prefix(combined, self.context_window)
self.carry = carry
let result = redact_with_config(stable, self.config)
result.text
}
///|
pub fn StreamingRedactor::finish(
self : StreamingRedactor,
) -> String raise DeidError {
if self.closed {
""
} else {
self.closed = true
let result = redact_with_config(self.carry, self.config)
self.carry = ""
result.text
}
}
///|
pub fn redact_chunks(
chunks : Array[String],
policy : ChunkPolicy,
) -> String raise DeidError {
let stream = StreamingRedactor::new(policy.config, policy.context_window)
let output = StringBuilder()
for chunk in chunks {
output.write_string(stream.push(chunk))
output.write_string(policy.separator)
}
output.write_string(stream.finish())
output.to_string()
}
///|
pub fn redact_lines(
text : String,
config : RedactionConfig,
) -> Array[String] raise DeidError {
let lines = split_lines_with_offsets(text)
lines.map(line => redact_with_config(line.text, config).text)
}
///|
pub fn redact_lines_joined(
text : String,
config : RedactionConfig,
separator : String,
) -> String raise DeidError {
redact_lines(text, config).join(separator)
}
///|
pub fn redact_chunk_results(
items : Array[BatchItem],
config : RedactionConfig,
) -> Array[ChunkResult] raise DeidError {
let results = []
for item in items {
let result = redact_with_config(item.text, config)
results.push({
chunk_id: item.id,
original_length: item.text.length(),
redacted_length: result.text.length(),
result,
})
}
results
}
///|
pub fn chunk_results_text(
results : Array[ChunkResult],
separator : String,
) -> String {
results.map(fn(item) { item.result.text }).join(separator)
}
///|
pub fn chunk_results_findings(results : Array[ChunkResult]) -> Array[Finding] {
let findings = []
let mut offset = 0
for item in results {
findings.append(shift_findings(item.result.findings, offset))
offset += item.original_length
}
findings
}
///|
pub fn chunk_policy_summary(policy : ChunkPolicy) -> String {
[
"context_window=\{policy.context_window}",
"separator=\{quote_for_log(policy.separator)}",
policy_summary(policy.config.policy),
].join("\n")
}
///|
pub fn split_fixed_chunks(text : String, width : Int) -> Array[String] {
if width <= 0 || text.is_empty() {
[text]
} else {
let chunks = []
let mut start = 0
while start < text.length() {
let end = if start + width > text.length() {
text.length()
} else {
start + width
}
chunks.push(text[start:end].to_owned())
start = end
}
chunks
}
}
///|
pub fn split_on_blank_lines(text : String) -> Array[String] {
text.split("\n\n").map(fn(item) { item.to_owned() }).to_array()
}
///|
pub fn stream_has_sensitive_boundary(
text : String,
rules : Array[Rule],
) -> Bool raise DeidError {
let findings = scan(text, rules~)
findings.any(fn(item) { item.start == 0 || item.end == text.length() })
}
///|
pub fn carry_for_boundary(text : String, width : Int) -> String {
if width <= 0 {
""
} else if text.length() <= width {
text
} else {
text[text.length() - width:].to_owned()
}
}