diff --git a/ext/jsonnet/vm.c b/ext/jsonnet/vm.c index 54a2cee..22b3626 100644 --- a/ext/jsonnet/vm.c +++ b/ext/jsonnet/vm.c @@ -20,8 +20,10 @@ static VALUE cVM; * Raised on evaluation errors in a Jsonnet VM. */ static VALUE eEvaluationError; +static VALUE eFormatError; static void raise_eval_error(struct JsonnetVm *vm, char *msg, rb_encoding *enc); +static void raise_format_error(struct JsonnetVm *vm, char *msg, rb_encoding *enc); static VALUE str_new_json(struct JsonnetVm *vm, char *json, rb_encoding *enc); static VALUE fileset_new(struct JsonnetVm *vm, char *buf, rb_encoding *enc); @@ -255,6 +257,128 @@ vm_set_max_trace(VALUE self, VALUE val) return Qnil; } +static VALUE +vm_set_fmt_indent(VALUE self, VALUE val) +{ + struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); + jsonnet_fmt_indent(vm->vm, NUM2INT(val)); + return val; +} + +static VALUE +vm_set_fmt_max_blank_lines(VALUE self, VALUE val) +{ + struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); + jsonnet_fmt_max_blank_lines(vm->vm, NUM2INT(val)); + return val; +} + +static VALUE +vm_set_fmt_string(VALUE self, VALUE str) +{ + const char *ptr; + struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); + StringValue(str); + if (RSTRING_LEN(str) != 1) { + rb_raise(rb_eArgError, "fmt_string must have a length of 1"); + } + ptr = RSTRING_PTR(str); + switch (*ptr) { + case 'd': + case 's': + case 'l': + jsonnet_fmt_string(vm->vm, *ptr); + return str; + default: + rb_raise(rb_eArgError, "fmt_string only accepts 'd', 's', or 'l'"); + } +} + +static VALUE +vm_set_fmt_comment(VALUE self, VALUE str) +{ + const char *ptr; + struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); + StringValue(str); + if (RSTRING_LEN(str) != 1) { + rb_raise(rb_eArgError, "fmt_comment must have a length of 1"); + } + ptr = RSTRING_PTR(str); + switch (*ptr) { + case 'h': + case 's': + case 'l': + jsonnet_fmt_comment(vm->vm, *ptr); + return str; + default: + rb_raise(rb_eArgError, "fmt_comment only accepts 'h', 's', or 'l'"); + } +} + +static VALUE +vm_set_fmt_pad_arrays(VALUE self, VALUE val) +{ + struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); + jsonnet_fmt_pad_objects(vm->vm, RTEST(val) ? 1 : 0); + return val; +} + +static VALUE +vm_set_fmt_pad_objects(VALUE self, VALUE val) +{ + struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); + jsonnet_fmt_pad_objects(vm->vm, RTEST(val) ? 1 : 0); + return val; +} + +static VALUE +vm_set_fmt_pretty_field_names(VALUE self, VALUE val) +{ + struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); + jsonnet_fmt_pretty_field_names(vm->vm, RTEST(val) ? 1 : 0); + return val; +} + +static VALUE +vm_set_fmt_sort_imports(VALUE self, VALUE val) +{ + struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); + jsonnet_fmt_sort_imports(vm->vm, RTEST(val) ? 1 : 0); + return val; +} + +static VALUE +vm_fmt_file(VALUE self, VALUE fname, VALUE encoding) +{ + int error; + char *result; + rb_encoding *const enc = rb_to_encoding(encoding); + struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); + + FilePathValue(fname); + result = jsonnet_fmt_file(vm->vm, StringValueCStr(fname), &error); + if (error) { + raise_format_error(vm->vm, result, rb_enc_get(fname)); + } + return str_new_json(vm->vm, result, enc); +} + +static VALUE +vm_fmt_snippet(VALUE self, VALUE snippet, VALUE fname) +{ + int error; + char *result; + struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); + + rb_encoding *enc = rubyjsonnet_assert_asciicompat(StringValue(snippet)); + FilePathValue(fname); + result = jsonnet_fmt_snippet(vm->vm, StringValueCStr(fname), StringValueCStr(snippet), &error); + if (error) { + raise_format_error(vm->vm, result, rb_enc_get(fname)); + } + return str_new_json(vm->vm, result, enc); +} + void rubyjsonnet_init_vm(VALUE mJsonnet) { @@ -262,6 +386,8 @@ rubyjsonnet_init_vm(VALUE mJsonnet) rb_define_singleton_method(cVM, "new", vm_s_new, -1); rb_define_private_method(cVM, "eval_file", vm_evaluate_file, 3); rb_define_private_method(cVM, "eval_snippet", vm_evaluate, 3); + rb_define_private_method(cVM, "fmt_file", vm_fmt_file, 2); + rb_define_private_method(cVM, "fmt_snippet", vm_fmt_snippet, 2); rb_define_method(cVM, "ext_var", vm_ext_var, 2); rb_define_method(cVM, "ext_code", vm_ext_code, 2); rb_define_method(cVM, "tla_var", vm_tla_var, 2); @@ -272,22 +398,30 @@ rubyjsonnet_init_vm(VALUE mJsonnet) rb_define_method(cVM, "gc_growth_trigger=", vm_set_gc_growth_trigger, 1); rb_define_method(cVM, "string_output=", vm_set_string_output, 1); rb_define_method(cVM, "max_trace=", vm_set_max_trace, 1); + rb_define_method(cVM, "fmt_indent=", vm_set_fmt_indent, 1); + rb_define_method(cVM, "fmt_max_blank_lines=", vm_set_fmt_max_blank_lines, 1); + rb_define_method(cVM, "fmt_string=", vm_set_fmt_string, 1); + rb_define_method(cVM, "fmt_comment=", vm_set_fmt_comment, 1); + rb_define_method(cVM, "fmt_pad_arrays=", vm_set_fmt_pad_arrays, 1); + rb_define_method(cVM, "fmt_pad_objects=", vm_set_fmt_pad_objects, 1); + rb_define_method(cVM, "fmt_pretty_field_names=", vm_set_fmt_pretty_field_names, 1); + rb_define_method(cVM, "fmt_sort_imports=", vm_set_fmt_sort_imports, 1); + + rb_define_const(mJsonnet, "STRING_STYLE_DOUBLE", rb_str_new_cstr("d")); + rb_define_const(mJsonnet, "STRING_STYLE_SINGLE", rb_str_new_cstr("s")); + rb_define_const(mJsonnet, "STRING_STYLE_LEAVE", rb_str_new_cstr("l")); + rb_define_const(mJsonnet, "COMMENT_STYLE_HASH", rb_str_new_cstr("h")); + rb_define_const(mJsonnet, "COMMENT_STYLE_SLASH", rb_str_new_cstr("s")); + rb_define_const(mJsonnet, "COMMENT_STYLE_LEAVE", rb_str_new_cstr("l")); rubyjsonnet_init_callbacks(cVM); eEvaluationError = rb_define_class_under(mJsonnet, "EvaluationError", rb_eRuntimeError); + eFormatError = rb_define_class_under(mJsonnet, "FormatError", rb_eRuntimeError); } -/** - * raises an EvaluationError whose message is \c msg. - * @param[in] vm a JsonnetVM - * @param[in] msg must be a NUL-terminated string returned by \c vm. - * @return never returns - * @throw EvaluationError - * @sa rescue_callback - */ static void -raise_eval_error(struct JsonnetVm *vm, char *msg, rb_encoding *enc) +raise_error(VALUE exception_class, struct JsonnetVm *vm, char *msg, rb_encoding *enc) { VALUE ex; const int state = rubyjsonnet_jump_tag(msg); @@ -300,11 +434,31 @@ raise_eval_error(struct JsonnetVm *vm, char *msg, rb_encoding *enc) rb_jump_tag(state); } - ex = rb_exc_new3(eEvaluationError, rb_enc_str_new_cstr(msg, enc)); + ex = rb_exc_new3(exception_class, rb_enc_str_new_cstr(msg, enc)); jsonnet_realloc(vm, msg, 0); rb_exc_raise(ex); } +/** + * raises an EvaluationError whose message is \c msg. + * @param[in] vm a JsonnetVM + * @param[in] msg must be a NUL-terminated string returned by \c vm. + * @return never returns + * @throw EvaluationError + * @sa rescue_callback + */ +static void +raise_eval_error(struct JsonnetVm *vm, char *msg, rb_encoding *enc) +{ + raise_error(eEvaluationError, vm, msg, enc); +} + +static void +raise_format_error(struct JsonnetVm *vm, char *msg, rb_encoding *enc) +{ + raise_error(eFormatError, vm, msg, enc); +} + /** * Returns a String whose contents is equal to \c json. * It automatically frees \c json just after constructing the return value. diff --git a/lib/jsonnet/vm.rb b/lib/jsonnet/vm.rb index ce1f839..151434e 100644 --- a/lib/jsonnet/vm.rb +++ b/lib/jsonnet/vm.rb @@ -88,6 +88,28 @@ def evaluate_file(filename, encoding: Encoding.default_external, multi: false) eval_file(filename, encoding, multi) end + ## + # Format Jsonnet file. + # + # @param [String] filename filename of a Jsonnet source file. + # @return [String] a formatted Jsonnet representation + # @raise [FormatError] raised when the formatting results an error. + def format_file(filename, encoding: Encoding.default_external) + fmt_file(filename, encoding) + end + + ## + # Format Jsonnet snippet. + # + # @param [String] jsonnet Jsonnet source string. Must be encoded in ASCII-compatible encoding. + # @param [String] filename filename of the source. Used in stacktrace. + # @return [String] a formatted Jsonnet representation + # @raise [FormatError] raised when the formatting results an error. + # @raise [UnsupportedEncodingError] raised when the encoding of jsonnt is not ASCII-compatible. + def format(jsonnet, filename: "(jsonnet)") + fmt_snippet(jsonnet, filename) + end + ## # Lets the given block handle "import" expression of Jsonnet. # @yieldparam [String] base base path to resolve "rel" from. diff --git a/test/test_vm.rb b/test/test_vm.rb index 975a100..35ce87b 100644 --- a/test/test_vm.rb +++ b/test/test_vm.rb @@ -474,6 +474,96 @@ class TestVM < Test::Unit::TestCase end end + test "Jsonnet::VM#format_file formats Jsonnet file" do + vm = Jsonnet::VM.new + vm.fmt_indent = 4 + with_example_file(%< + local myvar = 1; + { + "foo": myvar + } + >) {|fname| + result = vm.format_file(fname) + assert_equal <<-EOS, result +local myvar = 1; +{ + foo: myvar, +} + EOS + } + end + + test "Jsonnet::VM#format formats Jsonnet snippet" do + vm = Jsonnet::VM.new + vm.fmt_string = 'd' + result = vm.format(<<-EOS) +local myvar = 'myvar'; +{ +foo: [myvar,myvar] +} + EOS + assert_equal <<-EOS, result +local myvar = "myvar"; +{ + foo: [myvar, myvar], +} + EOS + end + + test "Jsonnet::VM#fmt_string only accepts 'd', 's', or 'l'" do + vm = Jsonnet::VM.new + vm.fmt_string = Jsonnet::STRING_STYLE_DOUBLE + vm.fmt_string = Jsonnet::STRING_STYLE_SINGLE + vm.fmt_string = Jsonnet::STRING_STYLE_LEAVE + assert_raise(ArgumentError) do + vm.fmt_string = '' + end + assert_raise(ArgumentError) do + vm.fmt_string = 'a' + end + assert_raise(ArgumentError) do + vm.fmt_string = 'ds' + end + assert_raise(TypeError) do + vm.fmt_string = 0 + end + end + + test "Jsonnet::VM#fmt_comment only accepts 'h', 's', or 'l'" do + vm = Jsonnet::VM.new + vm.fmt_comment = Jsonnet::COMMENT_STYLE_HASH + vm.fmt_comment = Jsonnet::COMMENT_STYLE_SLASH + vm.fmt_comment = Jsonnet::COMMENT_STYLE_LEAVE + assert_raise(ArgumentError) do + vm.fmt_comment = '' + end + assert_raise(ArgumentError) do + vm.fmt_comment = 'a' + end + assert_raise(ArgumentError) do + vm.fmt_comment = 'hs' + end + assert_raise(TypeError) do + vm.fmt_comment = 0 + end + end + + test "Jsonnet::VM#fmt_file raises FormatError on error" do + vm = Jsonnet::VM.new + with_example_file('{foo: }') do |fname| + assert_raise(Jsonnet::FormatError) do + vm.format_file(fname) + end + end + end + + test "Jsonnet::VM#fmt_snippet raises FormatError on error" do + vm = Jsonnet::VM.new + assert_raise(Jsonnet::FormatError) do + vm.format('{foo: }') + end + end + private def with_example_file(content) Tempfile.open("example.jsonnet") {|f|