1use std::borrow::Cow;
2
3use crate::{app_config::AppConfig, utils::static_filename};
4use anyhow::Context as _;
5use handlebars::{
6 handlebars_helper, Context, Handlebars, HelperDef, JsonTruthy, PathAndJson, RenderError,
7 RenderErrorReason, Renderable, ScopedJson,
8};
9use serde_json::Value as JsonValue;
10
11type H0 = fn() -> JsonValue;
13type H = fn(&JsonValue) -> JsonValue;
15type EH = fn(&JsonValue) -> anyhow::Result<JsonValue>;
17type HH = fn(&JsonValue, &JsonValue) -> JsonValue;
19#[allow(clippy::upper_case_acronyms)]
21type HHH = fn(&JsonValue, &JsonValue, &JsonValue) -> JsonValue;
22
23pub fn register_all_helpers(h: &mut Handlebars<'_>, config: &AppConfig) {
24 let site_prefix = config.site_prefix.clone();
25
26 register_helper(h, "all", HelperCheckTruthy(false));
27 register_helper(h, "any", HelperCheckTruthy(true));
28
29 register_helper(h, "stringify", stringify_helper as H);
30 register_helper(h, "parse_json", parse_json_helper as EH);
31 register_helper(h, "default", default_helper as HH);
32 register_helper(h, "entries", entries_helper as H);
33 register_helper(h, "replace", replace_helper as HHH);
34 h.register_helper("delay", Box::new(delay_helper));
36 h.register_helper("flush_delayed", Box::new(flush_delayed_helper));
37 register_helper(h, "plus", plus_helper as HH);
38 register_helper(h, "minus", minus_helper as HH);
39 h.register_helper("sum", Box::new(sum_helper));
40 register_helper(h, "starts_with", starts_with_helper as HH);
41
42 register_helper(h, "to_array", to_array_helper as H);
44
45 handlebars_helper!(array_contains: |array: Json, element: Json| match array {
47 JsonValue::Array(arr) => arr.contains(element),
48 other => other == element
49 });
50 h.register_helper("array_contains", Box::new(array_contains));
51
52 handlebars_helper!(array_contains_case_insensitive: |array: Json, element: Json| {
54 match array {
55 JsonValue::Array(arr) => arr.iter().any(|v| json_eq_case_insensitive(v, element)),
56 other => json_eq_case_insensitive(other, element),
57 }
58 });
59 h.register_helper(
60 "array_contains_case_insensitive",
61 Box::new(array_contains_case_insensitive),
62 );
63
64 register_helper(h, "static_path", StaticPathHelper(site_prefix.clone()));
66 register_helper(h, "app_config", AppConfigHelper(config.clone()));
67
68 h.register_helper("icon_img", Box::new(IconImgHelper(site_prefix)));
70 register_helper(h, "markdown", MarkdownHelper::new(config));
71 register_helper(h, "buildinfo", buildinfo_helper as EH);
72 register_helper(h, "typeof", typeof_helper as H);
73 register_helper(h, "rfc2822_date", rfc2822_date_helper as EH);
74 register_helper(h, "url_encode", url_encode_helper as H);
75 register_helper(h, "csv_escape", csv_escape_helper as HH);
76}
77
78fn json_eq_case_insensitive(a: &JsonValue, b: &JsonValue) -> bool {
79 match (a, b) {
80 (JsonValue::String(a), JsonValue::String(b)) => a.eq_ignore_ascii_case(b),
81 _ => a == b,
82 }
83}
84
85fn stringify_helper(v: &JsonValue) -> JsonValue {
86 v.to_string().into()
87}
88
89fn parse_json_helper(v: &JsonValue) -> Result<JsonValue, anyhow::Error> {
90 Ok(match v {
91 serde_json::value::Value::String(s) => serde_json::from_str(s)?,
92 other => other.clone(),
93 })
94}
95
96fn default_helper(v: &JsonValue, default: &JsonValue) -> JsonValue {
97 if v.is_null() {
98 default.clone()
99 } else {
100 v.clone()
101 }
102}
103
104fn plus_helper(a: &JsonValue, b: &JsonValue) -> JsonValue {
105 if let (Some(a), Some(b)) = (a.as_i64(), b.as_i64()) {
106 (a + b).into()
107 } else if let (Some(a), Some(b)) = (a.as_f64(), b.as_f64()) {
108 (a + b).into()
109 } else {
110 JsonValue::Null
111 }
112}
113
114fn minus_helper(a: &JsonValue, b: &JsonValue) -> JsonValue {
115 if let (Some(a), Some(b)) = (a.as_i64(), b.as_i64()) {
116 (a - b).into()
117 } else if let (Some(a), Some(b)) = (a.as_f64(), b.as_f64()) {
118 (a - b).into()
119 } else {
120 JsonValue::Null
121 }
122}
123
124fn starts_with_helper(a: &JsonValue, b: &JsonValue) -> JsonValue {
125 if let (Some(a), Some(b)) = (a.as_str(), b.as_str()) {
126 a.starts_with(b)
127 } else if let (Some(arr1), Some(arr2)) = (a.as_array(), b.as_array()) {
128 arr1.starts_with(arr2)
129 } else {
130 false
131 }
132 .into()
133}
134fn entries_helper(v: &JsonValue) -> JsonValue {
135 match v {
136 serde_json::value::Value::Object(map) => map
137 .into_iter()
138 .map(|(k, v)| serde_json::json!({"key": k, "value": v}))
139 .collect(),
140 serde_json::value::Value::Array(values) => values
141 .iter()
142 .enumerate()
143 .map(|(k, v)| serde_json::json!({"key": k, "value": v}))
144 .collect(),
145 _ => vec![],
146 }
147 .into()
148}
149
150fn to_array_helper(v: &JsonValue) -> JsonValue {
151 match v {
152 JsonValue::Array(arr) => arr.clone(),
153 JsonValue::Null => vec![],
154 JsonValue::String(s) if s.starts_with('[') => {
155 if let Ok(JsonValue::Array(r)) = serde_json::from_str(s) {
156 r
157 } else {
158 vec![JsonValue::String(s.clone())]
159 }
160 }
161 other => vec![other.clone()],
162 }
163 .into()
164}
165
166struct StaticPathHelper(String);
168
169impl CanHelp for StaticPathHelper {
170 fn call(&self, args: &[PathAndJson]) -> Result<JsonValue, String> {
171 let static_file = match args {
172 [v] => v.value(),
173 _ => return Err("expected one argument".to_string()),
174 };
175 let name = static_file
176 .as_str()
177 .ok_or_else(|| format!("static_path: not a string: {static_file}"))?;
178 let path = match name {
179 "sqlpage.js" => static_filename!("sqlpage.js"),
180 "sqlpage.css" => static_filename!("sqlpage.css"),
181 "apexcharts.js" => static_filename!("apexcharts.js"),
182 "tomselect.js" => static_filename!("tomselect.js"),
183 "favicon.svg" => static_filename!("favicon.svg"),
184 other => return Err(format!("unknown static file: {other:?}")),
185 };
186 Ok(format!("{}{}", self.0, path).into())
187 }
188}
189
190struct AppConfigHelper(AppConfig);
192
193impl CanHelp for AppConfigHelper {
194 fn call(&self, args: &[PathAndJson]) -> Result<JsonValue, String> {
195 let static_file = match args {
196 [v] => v.value(),
197 _ => return Err("expected one argument".to_string()),
198 };
199 let name = static_file
200 .as_str()
201 .ok_or_else(|| format!("app_config: not a string: {static_file}"))?;
202 match name {
203 "max_uploaded_file_size" => Ok(JsonValue::Number(self.0.max_uploaded_file_size.into())),
204 "environment" => serde_json::to_value(self.0.environment).map_err(|e| e.to_string()),
205 "site_prefix" => Ok(self.0.site_prefix.clone().into()),
206 other => Err(format!("unknown app config property: {other:?}")),
207 }
208 }
209}
210
211struct IconImgHelper(String);
213impl HelperDef for IconImgHelper {
214 fn call<'reg: 'rc, 'rc>(
215 &self,
216 helper: &handlebars::Helper<'rc>,
217 _r: &'reg Handlebars<'reg>,
218 _ctx: &'rc Context,
219 _rc: &mut handlebars::RenderContext<'reg, 'rc>,
220 writer: &mut dyn handlebars::Output,
221 ) -> handlebars::HelperResult {
222 let null = handlebars::JsonValue::Null;
223 let params = [0, 1].map(|i| helper.params().get(i).map_or(&null, PathAndJson::value));
224 let name = match params[0] {
225 JsonValue::String(s) => s,
226 other => {
227 log::debug!("icon_img: {other:?} is not an icon name, not rendering anything");
228 return Ok(());
229 }
230 };
231 let size = params[1].as_u64().unwrap_or(24);
232 write!(
233 writer,
234 "<svg width={size} height={size}><use href=\"{}{}#tabler-{name}\" /></svg>",
235 self.0,
236 static_filename!("tabler-icons.svg")
237 )?;
238 Ok(())
239 }
240}
241
242fn typeof_helper(v: &JsonValue) -> JsonValue {
243 match v {
244 JsonValue::Null => "null",
245 JsonValue::Bool(_) => "boolean",
246 JsonValue::Number(_) => "number",
247 JsonValue::String(_) => "string",
248 JsonValue::Array(_) => "array",
249 JsonValue::Object(_) => "object",
250 }
251 .into()
252}
253
254pub trait MarkdownConfig {
255 fn allow_dangerous_html(&self) -> bool;
256 fn allow_dangerous_protocol(&self) -> bool;
257}
258
259impl MarkdownConfig for AppConfig {
260 fn allow_dangerous_html(&self) -> bool {
261 self.markdown_allow_dangerous_html
262 }
263
264 fn allow_dangerous_protocol(&self) -> bool {
265 self.markdown_allow_dangerous_protocol
266 }
267}
268
269#[derive(Default)]
271struct MarkdownHelper {
272 allow_dangerous_html: bool,
273 allow_dangerous_protocol: bool,
274}
275
276impl MarkdownHelper {
277 fn new(config: &impl MarkdownConfig) -> Self {
278 Self {
279 allow_dangerous_html: config.allow_dangerous_html(),
280 allow_dangerous_protocol: config.allow_dangerous_protocol(),
281 }
282 }
283
284 fn get_preset_options(&self, preset_name: &str) -> Result<markdown::Options, String> {
285 let mut options = markdown::Options::gfm();
286 options.compile.allow_dangerous_html = self.allow_dangerous_html;
287 options.compile.allow_dangerous_protocol = self.allow_dangerous_protocol;
288 options.compile.allow_any_img_src = true;
289
290 match preset_name {
291 "default" => {}
292 "allow_unsafe" => {
293 options.compile.allow_dangerous_html = true;
294 options.compile.allow_dangerous_protocol = true;
295 }
296 _ => return Err(format!("unknown markdown preset: {preset_name}")),
297 }
298
299 Ok(options)
300 }
301}
302
303impl CanHelp for MarkdownHelper {
304 fn call(&self, args: &[PathAndJson]) -> Result<JsonValue, String> {
305 let (markdown_src_value, preset_name) = match args {
306 [v] => (v.value(), "default"),
307 [v, preset] => {
308 let value = v.value();
309 let preset_name_value = preset.value();
310 let preset = preset_name_value.as_str()
311 .ok_or_else(|| format!("markdown template helper expects a string as preset name. Got: {preset_name_value}"))?;
312 (value, preset)
313 }
314 _ => return Err("markdown template helper expects one or two arguments".to_string()),
315 };
316 let markdown_src = match markdown_src_value {
317 JsonValue::String(s) => Cow::Borrowed(s),
318 JsonValue::Array(arr) => Cow::Owned(
319 arr.iter()
320 .map(|v| v.as_str().unwrap_or_default())
321 .collect::<Vec<_>>()
322 .join("\n"),
323 ),
324 JsonValue::Null => Cow::Owned(String::new()),
325 other => Cow::Owned(other.to_string()),
326 };
327
328 let options = self.get_preset_options(preset_name)?;
329 markdown::to_html_with_options(&markdown_src, &options)
330 .map(JsonValue::String)
331 .map_err(|e| e.to_string())
332 }
333}
334
335fn buildinfo_helper(x: &JsonValue) -> anyhow::Result<JsonValue> {
336 match x {
337 JsonValue::String(s) if s == "CARGO_PKG_NAME" => Ok(env!("CARGO_PKG_NAME").into()),
338 JsonValue::String(s) if s == "CARGO_PKG_VERSION" => Ok(env!("CARGO_PKG_VERSION").into()),
339 other => Err(anyhow::anyhow!("unknown buildinfo key: {other:?}")),
340 }
341}
342
343fn rfc2822_date_helper(v: &JsonValue) -> anyhow::Result<JsonValue> {
345 let date: chrono::DateTime<chrono::FixedOffset> = match v {
346 JsonValue::String(s) => {
347 chrono::DateTime::parse_from_rfc3339(s)
349 .or_else(|_| {
350 chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d")
351 .map(|d| d.and_hms_opt(0, 0, 0).unwrap().and_utc().fixed_offset())
352 })
353 .with_context(|| format!("invalid date: {s}"))?
354 }
355 JsonValue::Number(n) => {
356 chrono::DateTime::from_timestamp(n.as_i64().with_context(|| "not a timestamp")?, 0)
357 .with_context(|| "invalid timestamp")?
358 .into()
359 }
360 other => anyhow::bail!("expected a date, got {other:?}"),
361 };
362 Ok(date.format("%a, %d %b %Y %T %z").to_string().into())
364}
365
366fn url_encode_helper(v: &JsonValue) -> JsonValue {
368 let as_str = match v {
369 JsonValue::String(s) => s,
370 other => &other.to_string(),
371 };
372 percent_encoding::percent_encode(as_str.as_bytes(), percent_encoding::NON_ALPHANUMERIC)
373 .to_string()
374 .into()
375}
376
377fn csv_escape_helper(v: &JsonValue, separator: &JsonValue) -> JsonValue {
379 let as_str = match v {
380 JsonValue::String(s) => s,
381 other => &other.to_string(),
382 };
383 let separator = separator.as_str().unwrap_or(",");
384 if as_str.contains(separator) || as_str.contains('"') || as_str.contains('\n') {
385 format!(r#""{}""#, as_str.replace('"', r#""""#)).into()
386 } else {
387 as_str.to_owned().into()
388 }
389}
390
391fn with_each_block<'a, 'reg, 'rc>(
392 rc: &'a mut handlebars::RenderContext<'reg, 'rc>,
393 mut action: impl FnMut(&mut handlebars::BlockContext<'rc>, bool) -> Result<(), RenderError>,
394) -> Result<(), RenderError> {
395 let mut blks = Vec::new();
396 while let Some(mut top) = rc.block_mut().map(std::mem::take) {
397 rc.pop_block();
398 action(&mut top, rc.block().is_none())?;
399 blks.push(top);
400 }
401 while let Some(blk) = blks.pop() {
402 rc.push_block(blk);
403 }
404 Ok(())
405}
406
407pub(crate) const DELAYED_CONTENTS: &str = "_delayed_contents";
408
409fn delay_helper<'reg, 'rc>(
410 h: &handlebars::Helper<'rc>,
411 r: &'reg Handlebars<'reg>,
412 ctx: &'rc Context,
413 rc: &mut handlebars::RenderContext<'reg, 'rc>,
414 _out: &mut dyn handlebars::Output,
415) -> handlebars::HelperResult {
416 let inner = h
417 .template()
418 .ok_or(RenderErrorReason::BlockContentRequired)?;
419 let mut str_out = handlebars::StringOutput::new();
420 inner.render(r, ctx, rc, &mut str_out)?;
421 let mut delayed_render = str_out.into_string()?;
422 with_each_block(rc, |block, is_last| {
423 if is_last {
424 let old_delayed_render = block
425 .get_local_var(DELAYED_CONTENTS)
426 .and_then(JsonValue::as_str)
427 .unwrap_or_default();
428 delayed_render += old_delayed_render;
429 let contents = JsonValue::String(std::mem::take(&mut delayed_render));
430 block.set_local_var(DELAYED_CONTENTS, contents);
431 }
432 Ok(())
433 })?;
434 Ok(())
435}
436
437fn flush_delayed_helper<'reg, 'rc>(
438 _h: &handlebars::Helper<'rc>,
439 _r: &'reg Handlebars<'reg>,
440 _ctx: &'rc Context,
441 rc: &mut handlebars::RenderContext<'reg, 'rc>,
442 writer: &mut dyn handlebars::Output,
443) -> handlebars::HelperResult {
444 with_each_block(rc, |block_context, _last| {
445 let delayed = block_context
446 .get_local_var(DELAYED_CONTENTS)
447 .and_then(JsonValue::as_str)
448 .filter(|s| !s.is_empty());
449 if let Some(contents) = delayed {
450 writer.write(contents)?;
451 block_context.set_local_var(DELAYED_CONTENTS, JsonValue::Null);
452 }
453 Ok(())
454 })
455}
456
457fn sum_helper<'reg, 'rc>(
458 helper: &handlebars::Helper<'rc>,
459 _r: &'reg Handlebars<'reg>,
460 _ctx: &'rc Context,
461 _rc: &mut handlebars::RenderContext<'reg, 'rc>,
462 writer: &mut dyn handlebars::Output,
463) -> handlebars::HelperResult {
464 let mut sum = 0f64;
465 for v in helper.params() {
466 sum += v
467 .value()
468 .as_f64()
469 .ok_or(RenderErrorReason::InvalidParamType("number"))?;
470 }
471 write!(writer, "{sum}")?;
472 Ok(())
473}
474
475pub struct HelperCheckTruthy(bool);
479
480impl CanHelp for HelperCheckTruthy {
481 fn call(&self, args: &[PathAndJson]) -> Result<JsonValue, String> {
482 for arg in args {
483 if arg.value().is_truthy(false) == self.0 {
484 return Ok(arg.value().clone());
485 }
486 }
487 if let Some(last) = args.last() {
488 Ok(last.value().clone())
489 } else {
490 Err("expected at least one argument".to_string())
491 }
492 }
493}
494
495trait CanHelp: Send + Sync + 'static {
496 fn call(&self, v: &[PathAndJson]) -> Result<JsonValue, String>;
497}
498
499impl CanHelp for H0 {
500 fn call(&self, args: &[PathAndJson]) -> Result<JsonValue, String> {
501 match args {
502 [] => Ok(self()),
503 _ => Err("expected no arguments".to_string()),
504 }
505 }
506}
507
508impl CanHelp for H {
509 fn call(&self, args: &[PathAndJson]) -> Result<JsonValue, String> {
510 match args {
511 [v] => Ok(self(v.value())),
512 _ => Err("expected one argument".to_string()),
513 }
514 }
515}
516
517impl CanHelp for EH {
518 fn call(&self, args: &[PathAndJson]) -> Result<JsonValue, String> {
519 match args {
520 [v] => self(v.value()).map_err(|e| e.to_string()),
521 _ => Err("expected one argument".to_string()),
522 }
523 }
524}
525
526impl CanHelp for HH {
527 fn call(&self, args: &[PathAndJson]) -> Result<JsonValue, String> {
528 match args {
529 [a, b] => Ok(self(a.value(), b.value())),
530 _ => Err("expected two arguments".to_string()),
531 }
532 }
533}
534
535impl CanHelp for HHH {
536 fn call(&self, args: &[PathAndJson]) -> Result<JsonValue, String> {
537 match args {
538 [a, b, c] => Ok(self(a.value(), b.value(), c.value())),
539 _ => Err("expected three arguments".to_string()),
540 }
541 }
542}
543
544struct JFun<F: CanHelp> {
545 name: &'static str,
546 fun: F,
547}
548impl<F: CanHelp> handlebars::HelperDef for JFun<F> {
549 fn call_inner<'reg: 'rc, 'rc>(
550 &self,
551 helper: &handlebars::Helper<'rc>,
552 _r: &'reg Handlebars<'reg>,
553 _: &'rc Context,
554 _rc: &mut handlebars::RenderContext<'reg, 'rc>,
555 ) -> Result<handlebars::ScopedJson<'rc>, RenderError> {
556 let result = self
557 .fun
558 .call(helper.params().as_slice())
559 .map_err(|s| RenderErrorReason::Other(format!("{}: {}", self.name, s)))?;
560 Ok(ScopedJson::Derived(result))
561 }
562}
563
564fn register_helper(h: &mut Handlebars, name: &'static str, fun: impl CanHelp) {
565 h.register_helper(name, Box::new(JFun { name, fun }));
566}
567
568fn replace_helper(text: &JsonValue, original: &JsonValue, replacement: &JsonValue) -> JsonValue {
569 let text_str = match text {
570 JsonValue::String(s) => s,
571 other => &other.to_string(),
572 };
573 let original_str = match original {
574 JsonValue::String(s) => s,
575 other => &other.to_string(),
576 };
577 let replacement_str = match replacement {
578 JsonValue::String(s) => s,
579 other => &other.to_string(),
580 };
581
582 text_str.replace(original_str, replacement_str).into()
583}
584
585#[cfg(test)]
586mod tests {
587 use crate::template_helpers::{rfc2822_date_helper, CanHelp, MarkdownHelper};
588 use handlebars::{JsonValue, PathAndJson, ScopedJson};
589 use serde_json::Value;
590
591 const CONTENT_KEY: &str = "contents_md";
592
593 #[test]
594 fn test_rfc2822_date() {
595 assert_eq!(
596 rfc2822_date_helper(&JsonValue::String("1970-01-02T03:04:05+02:00".into()))
597 .unwrap()
598 .as_str()
599 .unwrap(),
600 "Fri, 02 Jan 1970 03:04:05 +0200"
601 );
602 assert_eq!(
603 rfc2822_date_helper(&JsonValue::String("1970-01-02".into()))
604 .unwrap()
605 .as_str()
606 .unwrap(),
607 "Fri, 02 Jan 1970 00:00:00 +0000"
608 );
609 }
610
611 #[test]
612 fn test_basic_gfm_markdown() {
613 let helper = MarkdownHelper::default();
614
615 let contents = Value::String("# Heading".to_string());
616 let actual = helper.call(&as_args(&contents)).unwrap();
617
618 assert_eq!(Some("<h1>Heading</h1>"), actual.as_str());
619 }
620
621 mod markdown_html_blocks {
624
625 use super::*;
626
627 const UNSAFE_MARKUP: &str = "<table><tr><td>";
628 const ESCAPED_UNSAFE_MARKUP: &str = "<table><tr><td>";
629 #[test]
630 fn test_html_blocks_with_various_settings() {
631 struct TestCase {
632 name: &'static str,
633 preset: Option<Value>,
634 expected_output: Result<&'static str, String>,
635 }
636
637 let helper = MarkdownHelper::default();
638 let content = contents();
639
640 let test_cases = [
641 TestCase {
642 name: "default settings",
643 preset: Some(Value::String("default".to_string())),
644 expected_output: Ok(ESCAPED_UNSAFE_MARKUP),
645 },
646 TestCase {
647 name: "allow_unsafe preset",
648 preset: Some(Value::String("allow_unsafe".to_string())),
649 expected_output: Ok(UNSAFE_MARKUP),
650 },
651 TestCase {
652 name: "undefined allow_unsafe",
653 preset: Some(Value::Null),
654 expected_output: Err(
655 "markdown template helper expects a string as preset name. Got: null"
656 .to_string(),
657 ),
658 },
659 TestCase {
660 name: "allow_unsafe is false",
661 preset: Some(Value::Bool(false)),
662 expected_output: Err(
663 "markdown template helper expects a string as preset name. Got: false"
664 .to_string(),
665 ),
666 },
667 ];
668
669 for case in test_cases {
670 let args = match case.preset {
671 None => &as_args(&content)[..],
672 Some(ref preset) => &as_args_with_unsafe(&content, preset)[..],
673 };
674
675 match helper.call(args) {
676 Ok(actual) => assert_eq!(
677 case.expected_output.unwrap(),
678 actual.as_str().unwrap(),
679 "Failed on case: {}",
680 case.name
681 ),
682 Err(e) => assert_eq!(
683 case.expected_output.unwrap_err(),
684 e,
685 "Failed on case: {}",
686 case.name
687 ),
688 }
689 }
690 }
691
692 fn as_args_with_unsafe<'a>(
693 contents: &'a Value,
694 allow_unsafe: &'a Value,
695 ) -> [PathAndJson<'a>; 2] {
696 [
697 as_helper_arg(CONTENT_KEY, contents),
698 as_helper_arg("allow_unsafe", allow_unsafe),
699 ]
700 }
701
702 fn contents() -> Value {
703 Value::String(UNSAFE_MARKUP.to_string())
704 }
705 }
706
707 fn as_args(contents: &Value) -> [PathAndJson; 1] {
708 [as_helper_arg(CONTENT_KEY, contents)]
709 }
710
711 fn as_helper_arg<'a>(path: &'a str, value: &'a Value) -> PathAndJson<'a> {
712 let json_context = as_json_context(path, value);
713 to_path_and_json(path, json_context)
714 }
715
716 fn to_path_and_json<'a>(path: &'a str, value: ScopedJson<'a>) -> PathAndJson<'a> {
717 PathAndJson::new(Some(path.to_string()), value)
718 }
719
720 fn as_json_context<'a>(path: &'a str, value: &'a Value) -> ScopedJson<'a> {
721 ScopedJson::Context(value, vec![path.to_string()])
722 }
723}