Skip to content

add 'json_stringify' and 'jaq' (jq) built-in functions #2557

@liquidaty

Description

@liquidaty

So I just did a little experiment by adding two functons to my own just repo: json_stringify() and jaq() (rust implementation of jq)

They worked fabulously. I would like to propose incorporating them into the official just.

These alone can fill a huge portion of the gap that is needed to make a no-shell option viable (in fact, that's what I used them for), such as #1570 and #2458, as well as related use cases discussed in #528, #537, #2379, #2080, and probably many others.

jq_filter := 'split(" ") | .[] | <whatever else I want to do here>'

jq_input := json_stringify(my_input_str)
jq_output := jaq(jq_input, jq_filter)

recipe:
    blah {{ jq_output }} blah

Granted, using jq is a particular approach that may or may suit the user and for any given case might not be the perfect choice compared with potential external alternatives-- but consider:

  • it is extremely versatile and does not require a shell
  • in may cases is provides a built-in solution where no other built-in solution exists and there is no assurance that an external solution exists
  • its implementation is clean and unintrusive (merely add a couple functions totalling 73 LOC in src/function.rs)
  • it is does not conflict with any future alternative other solution
  • it does not introduce any platform dependencies or conflicts (the added code compiled out-of-the-box even to wasm)
  • it incorporates a well-established and widely-used library (treating jq and jaq as one and the same for this purpose)

Thoughts?

FYI, the changes in function.rs were merely as follows (note: this is a super quick-and-dirty whip-up for illustrative purposes). Happy to submit a PR:

diff --git a/src/function.rs b/src/function.rs
index 66e7c6e..82c40fc 100644
--- a/src/function.rs
+++ b/src/function.rs
@@ -71,6 +71,8 @@ pub(crate) fn get(name: &str) -> Option<Function> {
     "invocation_directory_native" => Nullary(invocation_directory_native),
     "is_dependency" => Nullary(is_dependency),
     "join" => BinaryPlus(join),
+    "jaq" => Binary(jaq),
+    "json_stringify" => UnaryPlus(json),
     "just_executable" => Nullary(just_executable),
     "just_pid" => Nullary(just_pid),
     "justfile" => Nullary(justfile),
@@ -369,6 +371,60 @@ fn prepend(_context: Context, prefix: &str, s: &str) -> FunctionResult {
   )
 }

+ // invalid_date is a helper function for jaq, probably should be located elsewhere
+fn invalid_data(e: impl std::error::Error + Send + Sync + 'static) -> std::io::Error {
+   use std::io;
+    io::Error::new(io::ErrorKind::InvalidData, e)
+}
+
+fn jaq(_context: Context, input_str: &str, filter_str: &str) -> FunctionResult {
+   use jaq_core::{load, Compiler, Ctx, RcIter};
+   use jaq_json::Val;
+//   println!("input: {}", input_str);
+//   println!("filter: {}", filter_str);
+
+   let json = |s: String| {
+       use hifijson::token::Lex;
+       hifijson::SliceLexer::new(s.as_bytes())
+           .exactly_one(Val::parse)
+           .map_err(invalid_data)
+   };
+
+   let input = json(input_str.to_string());
+   let program = File { code: filter_str, path: () };
+
+   use load::{Arena, File, Loader};
+
+   let loader = Loader::new(jaq_std::defs().chain(jaq_json::defs()));
+   let arena = Arena::default();
+
+   // parse the filter
+   let modules = loader.load(&arena, program).unwrap();
+
+   // compile the filter
+   let filter = Compiler::default()
+     .with_funs(jaq_std::funs().chain(jaq_json::funs()))
+     .compile(modules)
+     .unwrap();
+
+   let inputs = RcIter::new(core::iter::empty());
+
+   // iterator over the output values
+   let mut out = filter.run((Ctx::new([], &inputs), input.unwrap()));
+
+   // collect result values, each on a separate line
+   let mut output_str = String::new();
+   while let Some(value) = out.next() {
+      output_str.push_str(value.unwrap().to_string().as_str());
+      output_str.push('\n');
+   }
+   if output_str.ends_with('\n') {
+     output_str.pop();
+   }
+
+   Ok(output_str)
+}
+
 fn join(_context: Context, base: &str, with: &str, and: &[String]) -> FunctionResult {
   let mut result = Utf8Path::new(base).join(with);
   for arg in and {
@@ -434,6 +490,22 @@ fn justfile_directory(context: Context) -> FunctionResult {
     })
 }

+
+fn json_stringify(_context: Context, first_arg: &str, more_args: &[String]) -> FunctionResult {
+  use serde_json::json;
+    let result = if more_args.is_empty() {
+        // If no additional arguments, return JSON stringified version of the first argument
+        json!(first_arg).to_string()
+    } else {
+        // If additional arguments exist, create a JSON array with the first argument followed by the additional arguments
+        let mut args = vec![first_arg.to_string()];
+        args.extend_from_slice(more_args);
+        json!(args).to_string()
+    };
+
+    Ok(result)
+}
+
 fn kebabcase(_context: Context, s: &str) -> FunctionResult {
   Ok(s.to_kebab_case())
 }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions