From c0f2839bcf6b6741414ec943c0dc0c71ec0f9dc0 Mon Sep 17 00:00:00 2001 From: Kohei Suzuki Date: Thu, 23 May 2019 16:02:12 +0900 Subject: [PATCH 1/5] Add JsonnetVM methods for formatting --- ext/jsonnet/vm.c | 151 +++++++++++++++++++++++++++++++++++++++++++--- lib/jsonnet/vm.rb | 22 +++++++ test/test_vm.rb | 84 ++++++++++++++++++++++++++ 3 files changed, 247 insertions(+), 10 deletions(-) diff --git a/ext/jsonnet/vm.c b/ext/jsonnet/vm.c index 54a2cee..e1412d8 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,112 @@ 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 val) +{ + char *str; + struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); + str = StringValueCStr(val); + if (str[0] == '\0' || str[1] != '\0' || (str[0] != 'd' && str[0] != 's' && str[0] != 'l')) { + rb_raise(rb_eArgError, "fmt_string only accepts 'd', 's', or 'l'"); + } + jsonnet_fmt_string(vm->vm, str[0]); + return val; +} + +static VALUE +vm_set_fmt_comment(VALUE self, VALUE val) +{ + char *str; + struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); + str = StringValueCStr(val); + if (str[0] == '\0' || str[1] != '\0' || (str[0] != 'h' && str[0] != 's' && str[0] != 'l')) { + rb_raise(rb_eArgError, "fmt_comment only accepts 'h', 's', or 'l'"); + } + jsonnet_fmt_comment(vm->vm, str[0]); + return val; +} + +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_set_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_set_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) { @@ -272,22 +380,25 @@ 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_method(cVM, "fmt_file", vm_set_fmt_file, 2); + rb_define_method(cVM, "fmt_snippet", vm_set_fmt_snippet, 2); 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 +411,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..122f0be 100644 --- a/test/test_vm.rb +++ b/test/test_vm.rb @@ -474,6 +474,90 @@ 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 = 'd' + vm.fmt_string = 's' + vm.fmt_string = 'l' + 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 + end + + test "Jsonnet::VM#fmt_comment only accepts 'h', 's', or 'l'" do + vm = Jsonnet::VM.new + vm.fmt_comment = 'h' + vm.fmt_comment = 's' + vm.fmt_comment = 'l' + 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 + 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| From b487f83ebef97a7b36727d193c820c2ed80634cb Mon Sep 17 00:00:00 2001 From: Kohei Suzuki Date: Thu, 2 Jul 2020 00:01:33 +0900 Subject: [PATCH 2/5] Fix function namings Co-authored-by: Yuki Yugui Sonoda --- ext/jsonnet/vm.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ext/jsonnet/vm.c b/ext/jsonnet/vm.c index e1412d8..93e3cfc 100644 --- a/ext/jsonnet/vm.c +++ b/ext/jsonnet/vm.c @@ -332,7 +332,7 @@ vm_set_fmt_sort_imports(VALUE self, VALUE val) } static VALUE -vm_set_fmt_file(VALUE self, VALUE fname, VALUE encoding) +vm_fmt_file(VALUE self, VALUE fname, VALUE encoding) { int error; char *result; @@ -348,7 +348,7 @@ vm_set_fmt_file(VALUE self, VALUE fname, VALUE encoding) } static VALUE -vm_set_fmt_snippet(VALUE self, VALUE snippet, VALUE fname) +vm_fmt_snippet(VALUE self, VALUE snippet, VALUE fname) { int error; char *result; @@ -388,8 +388,8 @@ rubyjsonnet_init_vm(VALUE mJsonnet) 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_method(cVM, "fmt_file", vm_set_fmt_file, 2); - rb_define_method(cVM, "fmt_snippet", vm_set_fmt_snippet, 2); + rb_define_method(cVM, "fmt_file", vm_fmt_file, 2); + rb_define_method(cVM, "fmt_snippet", vm_fmt_snippet, 2); rubyjsonnet_init_callbacks(cVM); From 9bab98fa474ad7f76ae99f3c42c5b585b53cd566 Mon Sep 17 00:00:00 2001 From: Kohei Suzuki Date: Thu, 2 Jul 2020 00:25:03 +0900 Subject: [PATCH 3/5] Make fmt_string/fmt_comment check more straightforward --- ext/jsonnet/vm.c | 48 ++++++++++++++++++++++++++++++++---------------- test/test_vm.rb | 6 ++++++ 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/ext/jsonnet/vm.c b/ext/jsonnet/vm.c index 93e3cfc..04bbce3 100644 --- a/ext/jsonnet/vm.c +++ b/ext/jsonnet/vm.c @@ -274,29 +274,45 @@ vm_set_fmt_max_blank_lines(VALUE self, VALUE val) } static VALUE -vm_set_fmt_string(VALUE self, VALUE val) +vm_set_fmt_string(VALUE self, VALUE str) { - char *str; + const char *ptr; struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); - str = StringValueCStr(val); - if (str[0] == '\0' || str[1] != '\0' || (str[0] != 'd' && str[0] != 's' && str[0] != 'l')) { - rb_raise(rb_eArgError, "fmt_string only accepts 'd', 's', or 'l'"); + 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'"); } - jsonnet_fmt_string(vm->vm, str[0]); - return val; } static VALUE -vm_set_fmt_comment(VALUE self, VALUE val) +vm_set_fmt_comment(VALUE self, VALUE str) { - char *str; + const char *ptr; struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); - str = StringValueCStr(val); - if (str[0] == '\0' || str[1] != '\0' || (str[0] != 'h' && str[0] != 's' && str[0] != 'l')) { - rb_raise(rb_eArgError, "fmt_comment only accepts 'h', 's', or 'l'"); + 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'"); } - jsonnet_fmt_comment(vm->vm, str[0]); - return val; } static VALUE @@ -342,7 +358,7 @@ vm_fmt_file(VALUE self, VALUE fname, VALUE encoding) FilePathValue(fname); result = jsonnet_fmt_file(vm->vm, StringValueCStr(fname), &error); if (error) { - raise_format_error(vm->vm, result, rb_enc_get(fname)); + raise_format_error(vm->vm, result, rb_enc_get(fname)); } return str_new_json(vm->vm, result, enc); } @@ -358,7 +374,7 @@ vm_fmt_snippet(VALUE self, VALUE snippet, VALUE fname) 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)); + raise_format_error(vm->vm, result, rb_enc_get(fname)); } return str_new_json(vm->vm, result, enc); } diff --git a/test/test_vm.rb b/test/test_vm.rb index 122f0be..2261bda 100644 --- a/test/test_vm.rb +++ b/test/test_vm.rb @@ -524,6 +524,9 @@ class TestVM < Test::Unit::TestCase 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 @@ -540,6 +543,9 @@ class TestVM < Test::Unit::TestCase 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 From e47a1a3c75ac8a625e671ffb2dcf43d2ed3717cb Mon Sep 17 00:00:00 2001 From: Kohei Suzuki Date: Thu, 2 Jul 2020 00:39:50 +0900 Subject: [PATCH 4/5] Define Ruby-level constant for format options These namings are taken from google/go-jsonnet. https://pkg.go.dev/github.com/google/go-jsonnet@v0.16.0/formatter?tab=doc --- ext/jsonnet/vm.c | 7 +++++++ test/test_vm.rb | 12 ++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/ext/jsonnet/vm.c b/ext/jsonnet/vm.c index 04bbce3..12e12cb 100644 --- a/ext/jsonnet/vm.c +++ b/ext/jsonnet/vm.c @@ -407,6 +407,13 @@ rubyjsonnet_init_vm(VALUE mJsonnet) rb_define_method(cVM, "fmt_file", vm_fmt_file, 2); rb_define_method(cVM, "fmt_snippet", vm_fmt_snippet, 2); + 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); diff --git a/test/test_vm.rb b/test/test_vm.rb index 2261bda..35ce87b 100644 --- a/test/test_vm.rb +++ b/test/test_vm.rb @@ -512,9 +512,9 @@ class TestVM < Test::Unit::TestCase test "Jsonnet::VM#fmt_string only accepts 'd', 's', or 'l'" do vm = Jsonnet::VM.new - vm.fmt_string = 'd' - vm.fmt_string = 's' - vm.fmt_string = 'l' + 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 @@ -531,9 +531,9 @@ class TestVM < Test::Unit::TestCase test "Jsonnet::VM#fmt_comment only accepts 'h', 's', or 'l'" do vm = Jsonnet::VM.new - vm.fmt_comment = 'h' - vm.fmt_comment = 's' - vm.fmt_comment = 'l' + 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 From a7ba4b2564d92088fb8d019b26eefecfbcbf77cf Mon Sep 17 00:00:00 2001 From: Kohei Suzuki Date: Fri, 3 Jul 2020 01:00:35 +0900 Subject: [PATCH 5/5] Make fmt_file and fmt_snippet private The public APIs are format_file and format respectively. --- ext/jsonnet/vm.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/jsonnet/vm.c b/ext/jsonnet/vm.c index 12e12cb..22b3626 100644 --- a/ext/jsonnet/vm.c +++ b/ext/jsonnet/vm.c @@ -386,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); @@ -404,8 +406,6 @@ rubyjsonnet_init_vm(VALUE mJsonnet) 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_method(cVM, "fmt_file", vm_fmt_file, 2); - rb_define_method(cVM, "fmt_snippet", vm_fmt_snippet, 2); rb_define_const(mJsonnet, "STRING_STYLE_DOUBLE", rb_str_new_cstr("d")); rb_define_const(mJsonnet, "STRING_STYLE_SINGLE", rb_str_new_cstr("s"));