├── .clang-format ├── .github └── workflows │ └── ruby.yml ├── .gitignore ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── ext └── jsonnet │ ├── .gitignore │ ├── callbacks.c │ ├── extconf.rb │ ├── helpers.c │ ├── jsonnet.c │ ├── jsonnet_values.c │ ├── ruby_jsonnet.h │ └── vm.c ├── jsonnet.gemspec ├── lib ├── jsonnet.rb └── jsonnet │ ├── version.rb │ └── vm.rb └── test ├── fixtures └── jpath.libsonnet ├── test_jsonnet.rb └── test_vm.rb /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: Google 3 | IndentWidth: 4 4 | ColumnLimit: 100 5 | --- 6 | Language: Cpp 7 | PointerAlignment: Right 8 | AlwaysBreakAfterReturnType: AllDefinitions 9 | BreakBeforeBraces: Linux 10 | UseTab: Always 11 | TabWidth: 8 12 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: Run Unit Tests 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | test: 14 | name: Test 15 | strategy: 16 | matrix: 17 | ruby-version: ['3.0', '3.1', '3.2', '3.3'] 18 | os: ['ubuntu-latest', 'macos-latest'] 19 | runs-on: ${{ matrix.os }} 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Ruby 24 | uses: ruby/setup-ruby@v1 25 | with: 26 | ruby-version: ${{ matrix.ruby-version }} 27 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 28 | - name: Run tests 29 | run: bundle exec rake test 30 | 31 | test-with-system-lib: 32 | name: Test with system library 33 | runs-on: ubuntu-latest 34 | 35 | steps: 36 | - uses: actions/checkout@v3 37 | - name: Set up Ruby 38 | uses: ruby/setup-ruby@v1 39 | with: 40 | ruby-version: 3.1 41 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 42 | - name: Prepare libjsonnet 43 | run: sudo apt install libjsonnet-dev 44 | - name: Run tests 45 | run: env JSONNET_USE_SYSTEM_LIBRARIES=1 bundle exec rake test 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | dist: xenial 3 | script: bundle exec rake test 4 | 5 | matrix: 6 | include: 7 | - os: linux 8 | rvm: 2.5.8 9 | - os: osx 10 | rvm: 2.5.8 11 | - os: linux 12 | rvm: 2.6.6 13 | - os: osx 14 | rvm: 2.6.6 15 | - os: linux 16 | rvm: 2.7.2 17 | - os: osx 18 | rvm: 2.7.2 19 | fast_finish: true 20 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in jsonnet.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Yuki Yugui Sonoda 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/yugui/ruby-jsonnet.svg?branch=master)](https://travis-ci.org/yugui/ruby-jsonnet) 2 | 3 | # Jsonnet 4 | 5 | [Jsonnet][] processor library. Wraps the official C++ implementation with a Ruby extension library. 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | ```ruby 12 | gem 'jsonnet' 13 | ``` 14 | 15 | And then execute: 16 | 17 | ```shell 18 | $ bundle install 19 | ``` 20 | 21 | Or install it yourself as: 22 | 23 | ```shell 24 | $ gem install jsonnet 25 | ``` 26 | 27 | By default this gem will compile and install Jsonnet (v0.18.0) as part of 28 | installation. However you can use the system version of Jsonnet if you prefer. 29 | This would be the recommended route if you want to use a different version 30 | of Jsonnet or are having problems installing this. 31 | 32 | To install libjsonnet: 33 | 34 | ```shell 35 | $ git clone https://github.com/google/jsonnet.git 36 | $ cd jsonnet 37 | $ make libjsonnet.so 38 | $ sudo cp libjsonnet.so /usr/local/lib/libjsonnet.so 39 | $ sudo cp include/libjsonnet.h /usr/local/include/libjsonnet.h 40 | ``` 41 | 42 | Note: /usr/local/lib and /usr/local/include are used as they are library lookup 43 | locations. You may need to adjust these for your system if you have errors 44 | running this gem saying it can't open libjsonnet.so - on Ubuntu for instance 45 | I found /lib worked when /usr/local/lib did not. 46 | 47 | To install this gem without jsonnet: 48 | 49 | Use `JSONNET_USE_SYSTEM_LIBRARIES` ENV var: 50 | 51 | ```shell 52 | $ JSONNET_USE_SYSTEM_LIBRARIES=1 bundle install 53 | ``` 54 | 55 | or, the `--use-system-libraries` option: 56 | 57 | 58 | ```shell 59 | gem install jsonnet -- --use-system-libraries 60 | ``` 61 | 62 | ## Usage 63 | 64 | Load the library with `require "jsonnet"` 65 | 66 | You can evaluate a string of Jsonnet using `Jsonnet.evaluate` 67 | 68 | ``` 69 | irb(main):002:0> Jsonnet.evaluate('{ foo: "bar" }') 70 | => {"foo"=>"bar"} 71 | ``` 72 | Or load a file using `Jsonnet.load` 73 | 74 | ``` 75 | irb(main):002:0> Jsonnet.load('example.jsonnet') 76 | => {"baz"=>1} 77 | ``` 78 | 79 | To get closer to the C++ interface you can create an instance of `Jsonnet::VM` 80 | 81 | ``` 82 | irb(main):002:0> vm = Jsonnet::VM.new 83 | => # 84 | irb(main):003:0> vm.evaluate('{ foo: "bar" }') 85 | => "{\n \"foo\": \"bar\"\n}\n" 86 | irb(main):004:0> vm.evaluate_file('example.jsonnet') 87 | => "{\n \"baz\": 1\n}\n" 88 | ``` 89 | 90 | ## Contributing 91 | 92 | 1. Fork it ( https://github.com/yugui/ruby-jsonnet/fork ) 93 | 2. Create your feature branch (`git checkout -b my-new-feature`) 94 | 3. Commit your changes (`git commit -am 'Add some feature'`) 95 | 4. Push to the branch (`git push origin my-new-feature`) 96 | 5. Create a new Pull Request 97 | 98 | [Jsonnet]: https://github.com/google/jsonnet 99 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require "rake/extensiontask" 3 | require 'rake/testtask' 4 | 5 | task :default => :compile 6 | 7 | Rake::ExtensionTask.new do |t| 8 | t.name = 'jsonnet_wrap' 9 | t.ext_dir = 'ext/jsonnet' 10 | t.lib_dir = 'lib/jsonnet' 11 | end 12 | 13 | Rake::TestTask.new('test' => 'compile') do |t| 14 | t.libs << 'test' 15 | t.verbose = true 16 | end 17 | -------------------------------------------------------------------------------- /ext/jsonnet/.gitignore: -------------------------------------------------------------------------------- 1 | Makefile 2 | *.o 3 | -------------------------------------------------------------------------------- /ext/jsonnet/callbacks.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | #include "ruby_jsonnet.h" 7 | 8 | /* Magic prefix which distinguishes Ruby-level non-exception global escapes 9 | * from other errors in Jsonnet. 10 | * Any other errors in Jsonnet evaluation cannot contain this fragment of message. 11 | */ 12 | #define RUBYJSONNET_GLOBAL_ESCAPE_MAGIC "\x07\x03\x0c:rubytag:\x07\x03\x0c:" 13 | 14 | /* Copied from vm_core.h in Ruby. State variables in global escapes have 15 | * this value when an exception is raised. 16 | * 17 | * TODO(yugui) Find a better way to distinguish "raise" from "throw". 18 | * It is not a very good idea to depend on the implementation details of Ruby. 19 | */ 20 | #define RUBY_TAG_RAISE 0x6 21 | 22 | /* 23 | * callback support in VM 24 | */ 25 | 26 | static ID id_call; 27 | 28 | /* 29 | * Just invokes a callback with arguments, but also adapts the invocation to rb_protect. 30 | * @param[in] args list of arguments whose first value is the callable object itself. 31 | * @return result of the callback 32 | */ 33 | static VALUE 34 | invoke_callback(VALUE args) 35 | { 36 | long len = RARRAY_LEN(args); 37 | VALUE callback = rb_ary_entry(args, 0); 38 | return rb_funcall2(callback, id_call, (int)(len - 1), RARRAY_PTR(args) + 1); 39 | } 40 | 41 | /* 42 | * Handles long jump caught by rb_protect() in a callback function. 43 | * 44 | * Returns an error message which represents rb_errinfo(). 45 | * It is the caller's responsibility to return the message to the Jsonnet VM 46 | * in the right way. 47 | * Also the caller of the VM must handle evaluation failure caused by the 48 | * error message. 49 | * \sa rubyjsonnet_jump_tag 50 | * \sa raise_eval_error 51 | */ 52 | static VALUE 53 | rescue_callback(int state, const char *fmt, ...) 54 | { 55 | if (state == RUBY_TAG_RAISE) { 56 | VALUE err = rb_errinfo(); 57 | VALUE msg = rb_protect(rubyjsonnet_format_exception, err, NULL); 58 | if (msg == Qnil) { 59 | va_list ap; 60 | va_start(ap, fmt); 61 | msg = rb_vsprintf(fmt, ap); 62 | va_end(ap); 63 | } 64 | rb_set_errinfo(Qnil); 65 | return msg; 66 | } 67 | 68 | /* 69 | * Other types of global escape. 70 | * Here, we'll report the state as an evaluation error to let 71 | * the Jsonnet VM clean up its internal resources. 72 | * But we'll translate the error into an non-exception global escape 73 | * in Ruby again in raise_eval_error(). 74 | */ 75 | return rb_sprintf("%s%d%s", RUBYJSONNET_GLOBAL_ESCAPE_MAGIC, state, 76 | RUBYJSONNET_GLOBAL_ESCAPE_MAGIC); 77 | } 78 | 79 | /* 80 | * Tries to extract a jump tag from a string representation encoded by 81 | * rescue_callback. 82 | * @retval zero if \a exc_mesg is not such a string representation. 83 | * @retval non-zero the extracted tag 84 | */ 85 | int 86 | rubyjsonnet_jump_tag(const char *exc_mesg) 87 | { 88 | const char *tag; 89 | #define JSONNET_RUNTIME_ERROR_PREFIX "RUNTIME ERROR: " 90 | if (strncmp(exc_mesg, JSONNET_RUNTIME_ERROR_PREFIX, strlen(JSONNET_RUNTIME_ERROR_PREFIX))) { 91 | return 0; 92 | } 93 | tag = strstr(exc_mesg + strlen(JSONNET_RUNTIME_ERROR_PREFIX), RUBYJSONNET_GLOBAL_ESCAPE_MAGIC); 94 | if (tag) { 95 | const char *const body = tag + strlen(RUBYJSONNET_GLOBAL_ESCAPE_MAGIC); 96 | char *last; 97 | long state = strtol(body, &last, 10); 98 | if (!strncmp(last, RUBYJSONNET_GLOBAL_ESCAPE_MAGIC, 99 | strlen(RUBYJSONNET_GLOBAL_ESCAPE_MAGIC)) && 100 | INT_MIN <= state && state <= INT_MAX) { 101 | return (int)state; 102 | } 103 | } 104 | return 0; 105 | } 106 | 107 | #ifdef HAVE_JSONNET_IMPORT_CALLBACK_0_19 108 | static int 109 | import_callback_entrypoint(void *ctx, const char *base, const char *rel, char **found_here, 110 | char **buf, size_t *buflen) 111 | #else 112 | static char * 113 | import_callback_entrypoint(void *ctx, const char *base, const char *rel, char **found_here, 114 | int *success) 115 | #endif 116 | { 117 | struct jsonnet_vm_wrap *const vm = (struct jsonnet_vm_wrap *)ctx; 118 | int state; 119 | VALUE result, args; 120 | 121 | args = rb_ary_tmp_new(3); 122 | 123 | rb_ary_push(args, vm->import_callback); 124 | rb_ary_push(args, rb_enc_str_new_cstr(base, rb_filesystem_encoding())); 125 | rb_ary_push(args, rb_enc_str_new_cstr(rel, rb_filesystem_encoding())); 126 | result = rb_protect(invoke_callback, args, &state); 127 | 128 | rb_ary_free(args); 129 | 130 | if (state) { 131 | VALUE msg = rescue_callback(state, "cannot import %s from %s", rel, base); 132 | #ifdef HAVE_JSONNET_IMPORT_CALLBACK_0_19 133 | *buf = rubyjsonnet_str_to_ptr(vm->vm, msg, buflen); 134 | return 1; 135 | #else 136 | *success = 0; 137 | return rubyjsonnet_str_to_cstr(vm->vm, msg); 138 | #endif 139 | } 140 | 141 | result = rb_Array(result); 142 | *found_here = rubyjsonnet_str_to_cstr(vm->vm, rb_ary_entry(result, 1)); 143 | #ifdef HAVE_JSONNET_IMPORT_CALLBACK_0_19 144 | *buf = rubyjsonnet_str_to_ptr(vm->vm, rb_ary_entry(result, 0), buflen); 145 | return 0; 146 | #else 147 | *success = 1; 148 | return rubyjsonnet_str_to_cstr(vm->vm, rb_ary_entry(result, 0)); 149 | #endif 150 | } 151 | 152 | /* 153 | * Sets a custom way to resolve "import" expression. 154 | * @param [#call] callback receives two parameters and returns two values. 155 | * The first parameter "base" is a base directory to resolve 156 | * "rel" from. 157 | * The second parameter "rel" is an absolute or a relative 158 | * path to the file to import. 159 | * The first return value is the content of the imported file. 160 | * The second return value is the resolved path of the imported file. 161 | */ 162 | static VALUE 163 | vm_set_import_callback(VALUE self, VALUE callback) 164 | { 165 | struct jsonnet_vm_wrap *const vm = rubyjsonnet_obj_to_vm(self); 166 | 167 | vm->import_callback = callback; 168 | jsonnet_import_callback(vm->vm, import_callback_entrypoint, vm); 169 | 170 | return callback; 171 | } 172 | 173 | /** 174 | * Generic entrypoint of native callbacks which adapts callable objects in Ruby to \c 175 | * JsonnetNativeCallback. 176 | * 177 | * @param[in] data pointer to a {\c struct native_callback_ctx} 178 | * @param[in] argv NULL-terminated array of arguments 179 | * @param[out] success set to 1 on success, or 0 if otherwise. 180 | * @returns the result of the callback on success, an error message on failure. 181 | */ 182 | static struct JsonnetJsonValue * 183 | native_callback_entrypoint(void *data, const struct JsonnetJsonValue *const *argv, int *success) 184 | { 185 | long i; 186 | int state = 0; 187 | 188 | struct native_callback_ctx *const ctx = (struct native_callback_ctx *)data; 189 | struct JsonnetVm *const vm = rubyjsonnet_obj_to_vm(ctx->vm)->vm; 190 | VALUE result, args = rb_ary_tmp_new(ctx->arity + 1); 191 | 192 | rb_ary_push(args, ctx->callback); 193 | for (i = 0; i < ctx->arity; ++i) { 194 | rb_ary_push(args, rubyjsonnet_json_to_obj(vm, argv[i])); 195 | } 196 | 197 | result = rb_protect(invoke_callback, args, &state); 198 | 199 | rb_ary_free(args); 200 | 201 | if (state) { 202 | VALUE msg = rescue_callback(state, "something wrong in %" PRIsVALUE, ctx->callback); 203 | *success = 0; 204 | return rubyjsonnet_obj_to_json(vm, msg, &state); 205 | } 206 | 207 | return rubyjsonnet_obj_to_json(vm, result, success); 208 | } 209 | 210 | /* 211 | * Registers a native extension written in Ruby. 212 | * @param callback [#call] a PURE callable object 213 | * @param params [Array] names of the parameters of the function 214 | */ 215 | static VALUE 216 | vm_register_native_callback(VALUE self, VALUE name, VALUE callback, VALUE params) 217 | { 218 | struct { 219 | volatile VALUE store; 220 | long len; 221 | const char **buf; 222 | } cstr_params; 223 | struct native_callback_ctx *ctx; 224 | struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); 225 | long i; 226 | 227 | name = rb_to_symbol(name); 228 | rubyjsonnet_assert_asciicompat(name); 229 | 230 | params = rb_Array(params); 231 | cstr_params.len = RARRAY_LEN(params); 232 | cstr_params.buf = (const char **)rb_alloc_tmp_buffer( 233 | &cstr_params.store, sizeof(const char *const) * (cstr_params.len + 1)); 234 | for (i = 0; i < cstr_params.len; ++i) { 235 | const VALUE pname = rb_to_symbol(RARRAY_AREF(params, i)); 236 | rubyjsonnet_assert_asciicompat(pname); 237 | cstr_params.buf[i] = rb_id2name(RB_SYM2ID(pname)); 238 | } 239 | cstr_params.buf[cstr_params.len] = NULL; 240 | 241 | ctx = RB_ALLOC_N(struct native_callback_ctx, 1); 242 | ctx->callback = callback; 243 | ctx->arity = cstr_params.len; 244 | ctx->vm = self; 245 | jsonnet_native_callback(vm->vm, rb_id2name(RB_SYM2ID(name)), native_callback_entrypoint, ctx, 246 | cstr_params.buf); 247 | 248 | rb_free_tmp_buffer(&cstr_params.store); 249 | 250 | RB_REALLOC_N(vm->native_callbacks.contexts, struct native_callback_ctx *, 251 | vm->native_callbacks.len + 1); 252 | vm->native_callbacks.contexts[vm->native_callbacks.len] = ctx; 253 | vm->native_callbacks.len++; 254 | 255 | return name; 256 | } 257 | 258 | void 259 | rubyjsonnet_init_callbacks(VALUE cVM) 260 | { 261 | id_call = rb_intern("call"); 262 | 263 | rb_define_method(cVM, "import_callback=", vm_set_import_callback, 1); 264 | rb_define_private_method(cVM, "register_native_callback", vm_register_native_callback, 3); 265 | } 266 | -------------------------------------------------------------------------------- /ext/jsonnet/extconf.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | require 'fileutils' 3 | 4 | def using_system_libraries? 5 | arg_config('--use-system-libraries', !!ENV['JSONNET_USE_SYSTEM_LIBRARIES']) 6 | end 7 | 8 | dir_config('jsonnet') 9 | 10 | unless using_system_libraries? 11 | message "Building jsonnet using packaged libraries.\n" 12 | require 'rubygems' 13 | require 'mini_portile2' 14 | message "Using mini_portile version #{MiniPortile::VERSION}\n" 15 | 16 | recipe = MiniPortile.new('jsonnet', 'v0.20.0') 17 | recipe.files = ['https://github.com/google/jsonnet/archive/v0.20.0.tar.gz'] 18 | class << recipe 19 | CORE_OBJS = %w[ 20 | desugarer.o formatter.o lexer.o libjsonnet.o parser.o pass.o static_analysis.o string_utils.o vm.o 21 | ].map {|name| File.join('core', name) } 22 | MD5_OBJS = %w[ 23 | md5.o 24 | ].map {|name| File.join('third_party', 'md5', name) } 25 | C4_CORE_OBJS = %w[ 26 | base64.o 27 | char_traits.o 28 | error.o 29 | format.o 30 | language.o 31 | memory_resource.o 32 | memory_util.o 33 | time.o 34 | ].map {|name| File.join('third_party', 'rapidyaml', 'rapidyaml', 'ext', 'c4core', 'src', 'c4', name) } 35 | RAPID_YAML_OBJS = %w[ 36 | common.o parse.o preprocess.o tree.o 37 | ].map {|name| File.join('third_party', 'rapidyaml', 'rapidyaml', 'src', 'c4', 'yml', name) } 38 | 39 | def compile 40 | # We want to create a file a library we can link to. Jsonnet provides us 41 | # with the command `make libjsonnet.so` which creates a shared object 42 | # however that won't be bundled into the compiled output so instead 43 | # we compile the c into .o files and then create an archive that can 44 | # be linked to 45 | execute('compile', make_cmd) 46 | execute('archive', 'ar rcs libjsonnet.a ' + target_object_files.join(' ')) 47 | end 48 | 49 | def configured? 50 | true 51 | end 52 | 53 | def install 54 | lib_path = File.join(port_path, 'lib') 55 | include_path = File.join(port_path, 'include') 56 | 57 | FileUtils.mkdir_p([lib_path, include_path]) 58 | 59 | FileUtils.cp(File.join(work_path, 'libjsonnet.a'), lib_path) 60 | FileUtils.cp(File.join(work_path, 'include', 'libjsonnet.h'), include_path) 61 | FileUtils.cp(File.join(work_path, 'include', 'libjsonnet_fmt.h'), include_path) 62 | end 63 | 64 | private 65 | def target_object_files 66 | if version >= 'v0.18.0' 67 | CORE_OBJS + MD5_OBJS + C4_CORE_OBJS + RAPID_YAML_OBJS 68 | else 69 | CORE_OBJS + MD5_OBJS 70 | end 71 | end 72 | end 73 | 74 | recipe.cook 75 | # I tried using recipe.activate here but that caused this file to build ok 76 | # but the makefile to fail. These commands add the necessary paths to do both 77 | $LIBPATH = ["#{recipe.path}/lib"] | $LIBPATH 78 | $CPPFLAGS << " -I#{recipe.path}/include" 79 | 80 | end 81 | 82 | # jsonnet_wrap extension must be linked with c++ stdlib because 83 | # the C++ library Rapid YAML is being statically linked. 84 | rbconfig = RbConfig::MAKEFILE_CONFIG 85 | if rbconfig['LDSHAREDXX'] 86 | rbconfig['LDSHARED'] = rbconfig['LDSHAREDXX'] 87 | end 88 | 89 | abort 'libjsonnet.h not found' unless have_header('libjsonnet.h') 90 | abort 'libjsonnet not found' unless have_library('jsonnet') 91 | have_header('libjsonnet_fmt.h') 92 | 93 | import_callback_0_19 = checking_for checking_message('JsonnetImportCallback >= v0.19.0') do 94 | try_compile(< 96 | 97 | int f(void *ctx, const char *base, const char *rel, char **found_here, char **buf, size_t *buflen); 98 | 99 | int main() { 100 | jsonnet_import_callback(NULL, f, NULL); 101 | return 0; 102 | } 103 | SRC 104 | end 105 | if import_callback_0_19 106 | $defs.push('-DHAVE_JSONNET_IMPORT_CALLBACK_0_19') 107 | end 108 | 109 | create_makefile('jsonnet/jsonnet_wrap') 110 | -------------------------------------------------------------------------------- /ext/jsonnet/helpers.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "ruby_jsonnet.h" 8 | 9 | static ID id_message; 10 | 11 | /* 12 | * Indicates that the encoding of the given string is not allowed by 13 | * the C++ implementation of Jsonnet. 14 | */ 15 | static VALUE eUnsupportedEncodingError; 16 | 17 | /** 18 | * Asserts that the given string-like object is in an 19 | * ASCII-compatible encoding. 20 | * 21 | * @param[in] str a String-like object 22 | * @throw UnsupportedEncodingError on assertion failure. 23 | */ 24 | rb_encoding * 25 | rubyjsonnet_assert_asciicompat(VALUE str) 26 | { 27 | rb_encoding *enc = rb_enc_get(str); 28 | if (!rb_enc_asciicompat(enc)) { 29 | rb_raise(eUnsupportedEncodingError, "jsonnet encoding must be ASCII-compatible but got %s", 30 | rb_enc_name(enc)); 31 | } 32 | return enc; 33 | } 34 | 35 | /** 36 | * Allocates a C string whose content is equal to \c str with jsonnet_realloc. 37 | * 38 | * Note that this function does not allow NUL characters in the string. 39 | * You should use rubyjsonnet_str_to_ptr() if you want to handle NUL characters. 40 | * 41 | * @param[in] vm a Jsonnet VM 42 | * @param[in] str a String-like object 43 | * @return the allocated C string 44 | */ 45 | char * 46 | rubyjsonnet_str_to_cstr(struct JsonnetVm *vm, VALUE str) 47 | { 48 | const char *const cstr = StringValueCStr(str); 49 | char *const buf = jsonnet_realloc(vm, NULL, strlen(cstr)); 50 | strcpy(buf, cstr); 51 | return buf; 52 | } 53 | 54 | /** 55 | * Allocates a byte sequence whose content is equal to \c str with jsonnet_realloc. 56 | * 57 | * @param[in] vm a Jsonnet VM 58 | * @param[in] str a String-like object 59 | * @param[out] buflen the length of the allocated buffer 60 | * @return the allocated buffer 61 | */ 62 | char * 63 | rubyjsonnet_str_to_ptr(struct JsonnetVm *vm, VALUE str, size_t *buflen) 64 | { 65 | StringValue(str); 66 | size_t len = RSTRING_LEN(str); 67 | char *buf = jsonnet_realloc(vm, NULL, len); 68 | memcpy(buf, RSTRING_PTR(str), len); 69 | *buflen = len; 70 | return buf; 71 | } 72 | 73 | /** 74 | * @return a human readable string which contains the class name of the 75 | * exception and its message. It might be nil on failure 76 | */ 77 | VALUE 78 | rubyjsonnet_format_exception(VALUE exc) 79 | { 80 | VALUE name = rb_class_name(rb_obj_class(exc)); 81 | VALUE msg = rb_funcall(exc, id_message, 0); 82 | if (RB_TYPE_P(name, RUBY_T_STRING) && rb_str_strlen(name)) { 83 | if (RB_TYPE_P(msg, RUBY_T_STRING) && rb_str_strlen(msg)) { 84 | return rb_str_concat(rb_str_cat_cstr(name, " : "), msg); 85 | } else { 86 | return name; 87 | } 88 | } else if (RB_TYPE_P(msg, RUBY_T_STRING) && rb_str_strlen(msg)) { 89 | return msg; 90 | } 91 | return Qnil; 92 | } 93 | 94 | void 95 | rubyjsonnet_init_helpers(VALUE mJsonnet) 96 | { 97 | id_message = rb_intern("message"); 98 | eUnsupportedEncodingError = 99 | rb_define_class_under(mJsonnet, "UnsupportedEncodingError", rb_eEncodingError); 100 | } 101 | -------------------------------------------------------------------------------- /ext/jsonnet/jsonnet.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "ruby_jsonnet.h" 6 | 7 | /* 8 | * call-seq: 9 | * Jsonnet.version -> String 10 | * 11 | * Returns the version of the underlying C++ implementation of Jsonnet. 12 | */ 13 | static VALUE 14 | jw_s_version(VALUE mod) 15 | { 16 | return rb_usascii_str_new_cstr(jsonnet_version()); 17 | } 18 | 19 | void 20 | Init_jsonnet_wrap(void) 21 | { 22 | VALUE mJsonnet = rb_define_module("Jsonnet"); 23 | rb_define_singleton_method(mJsonnet, "libversion", jw_s_version, 0); 24 | 25 | rubyjsonnet_init_helpers(mJsonnet); 26 | rubyjsonnet_init_vm(mJsonnet); 27 | } 28 | -------------------------------------------------------------------------------- /ext/jsonnet/jsonnet_values.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "ruby_jsonnet.h" 6 | 7 | static struct JsonnetJsonValue *protect_obj_to_json(struct JsonnetVm *vm, VALUE obj, 8 | struct JsonnetJsonValue *parent); 9 | 10 | /** 11 | * Converts a Jsonnet JSON value into a Ruby object. 12 | * 13 | * Arrays and objects in JSON are not supported due to the limitation of 14 | * libjsonnet API. 15 | */ 16 | VALUE 17 | rubyjsonnet_json_to_obj(struct JsonnetVm *vm, const struct JsonnetJsonValue *value) 18 | { 19 | union { 20 | const char *str; 21 | double num; 22 | } typed_value; 23 | 24 | if ((typed_value.str = jsonnet_json_extract_string(vm, value))) { 25 | return rb_enc_str_new_cstr(typed_value.str, rb_utf8_encoding()); 26 | } 27 | if (jsonnet_json_extract_number(vm, value, &typed_value.num)) { 28 | return DBL2NUM(typed_value.num); 29 | } 30 | 31 | switch (jsonnet_json_extract_bool(vm, value)) { 32 | case 0: 33 | return Qfalse; 34 | case 1: 35 | return Qtrue; 36 | case 2: 37 | break; 38 | default: 39 | /* never happens */ 40 | rb_raise(rb_eRuntimeError, "unrecognized json bool value"); 41 | } 42 | if (jsonnet_json_extract_null(vm, value)) { 43 | return Qnil; 44 | } 45 | 46 | /* TODO: support arrays and objects when they get accessible */ 47 | rb_raise(rb_eArgError, "unsupported type of JSON value"); 48 | } 49 | 50 | static struct JsonnetJsonValue * 51 | string_to_json(struct JsonnetVm *vm, VALUE str) 52 | { 53 | rubyjsonnet_assert_asciicompat(str); 54 | return jsonnet_json_make_string(vm, RSTRING_PTR(str)); 55 | } 56 | 57 | static struct JsonnetJsonValue * 58 | num_to_json(struct JsonnetVm *vm, VALUE n) 59 | { 60 | return jsonnet_json_make_number(vm, NUM2DBL(n)); 61 | } 62 | 63 | static struct JsonnetJsonValue * 64 | ary_to_json(struct JsonnetVm *vm, VALUE ary) 65 | { 66 | struct JsonnetJsonValue *const json_array = jsonnet_json_make_array(vm); 67 | 68 | int i; 69 | for (i = 0; i < RARRAY_LEN(ary); ++i) { 70 | struct JsonnetJsonValue *const v = protect_obj_to_json(vm, RARRAY_AREF(ary, i), json_array); 71 | jsonnet_json_array_append(vm, json_array, v); 72 | } 73 | return json_array; 74 | } 75 | 76 | struct hash_to_json_params { 77 | struct JsonnetVm *vm; 78 | struct JsonnetJsonValue *obj; 79 | }; 80 | 81 | static int 82 | hash_item_to_json(VALUE key, VALUE value, VALUE paramsval) 83 | { 84 | const struct hash_to_json_params *const params = (const struct hash_to_json_params *)paramsval; 85 | 86 | StringValue(key); 87 | rubyjsonnet_assert_asciicompat(key); 88 | 89 | jsonnet_json_object_append(params->vm, params->obj, StringValueCStr(key), 90 | protect_obj_to_json(params->vm, value, params->obj)); 91 | 92 | return ST_CONTINUE; 93 | } 94 | 95 | static struct JsonnetJsonValue * 96 | hash_to_json(struct JsonnetVm *vm, VALUE hash) 97 | { 98 | struct JsonnetJsonValue *const json_obj = jsonnet_json_make_object(vm); 99 | const struct hash_to_json_params args = {vm, json_obj}; 100 | rb_hash_foreach(hash, hash_item_to_json, (VALUE)&args); 101 | return json_obj; 102 | } 103 | 104 | /** 105 | * Converts a Ruby object into a Jsonnet JSON value 106 | * 107 | * TODO(yugui): Safely destorys an intermediate object on exception. 108 | */ 109 | static struct JsonnetJsonValue * 110 | obj_to_json(struct JsonnetVm *vm, VALUE obj) 111 | { 112 | VALUE converted; 113 | 114 | switch (obj) { 115 | case Qnil: 116 | return jsonnet_json_make_null(vm); 117 | case Qtrue: 118 | return jsonnet_json_make_bool(vm, 1); 119 | case Qfalse: 120 | return jsonnet_json_make_bool(vm, 0); 121 | } 122 | 123 | converted = rb_check_string_type(obj); 124 | if (converted != Qnil) { 125 | return string_to_json(vm, converted); 126 | } 127 | 128 | converted = rb_check_to_float(obj); 129 | if (converted != Qnil) { 130 | return num_to_json(vm, converted); 131 | } 132 | 133 | converted = rb_check_array_type(obj); 134 | if (converted != Qnil) { 135 | return ary_to_json(vm, converted); 136 | } 137 | 138 | converted = rb_check_hash_type(obj); 139 | if (converted != Qnil) { 140 | return hash_to_json(vm, converted); 141 | } 142 | 143 | converted = rb_any_to_s(obj); 144 | return string_to_json(vm, converted); 145 | } 146 | 147 | struct protect_args { 148 | struct JsonnetVm *vm; 149 | VALUE obj; 150 | }; 151 | 152 | static VALUE 153 | protect_obj_to_json_block(VALUE paramsval) 154 | { 155 | const struct protect_args *const params = (const struct protect_args *)paramsval; 156 | return (VALUE)obj_to_json(params->vm, params->obj); 157 | } 158 | 159 | /** 160 | * Safely converts a Ruby object into a JSON value. 161 | * 162 | * It automatically destroys \a parent on exception. 163 | * @param[in] vm a Jsonnet VM 164 | * @param[in] obj a Ruby object to be converted 165 | * @param[in] parent destroys this value on failure 166 | * @throws can throw \c TypeError or other exceptions 167 | */ 168 | static struct JsonnetJsonValue * 169 | protect_obj_to_json(struct JsonnetVm *vm, VALUE obj, struct JsonnetJsonValue *parent) 170 | { 171 | const struct protect_args args = {vm, obj}; 172 | int state = 0; 173 | 174 | VALUE result = rb_protect(protect_obj_to_json_block, (VALUE)&args, &state); 175 | if (!state) { 176 | return (struct JsonnetJsonValue *)result; 177 | } 178 | 179 | jsonnet_json_destroy(vm, parent); 180 | rb_jump_tag(state); 181 | } 182 | 183 | /** 184 | * Converts a Ruby object into a JSON value. 185 | * Returns an error message on failure. 186 | * 187 | * @param[in] vm a Jsonnet VM 188 | * @param[in] obj a Ruby object to be converted 189 | * @param[out] success set to 1 on success, set to 0 on failure. 190 | * @returns the converted value on success, an error message on failure. 191 | */ 192 | struct JsonnetJsonValue * 193 | rubyjsonnet_obj_to_json(struct JsonnetVm *vm, VALUE obj, int *success) 194 | { 195 | int state = 0; 196 | const struct protect_args args = {vm, obj}; 197 | VALUE result = rb_protect(protect_obj_to_json_block, (VALUE)&args, &state); 198 | if (state) { 199 | const VALUE msg = rubyjsonnet_format_exception(rb_errinfo()); 200 | rb_set_errinfo(Qnil); 201 | *success = 0; 202 | return string_to_json(vm, msg); 203 | } 204 | *success = 1; 205 | return (struct JsonnetJsonValue *)result; 206 | } 207 | -------------------------------------------------------------------------------- /ext/jsonnet/ruby_jsonnet.h: -------------------------------------------------------------------------------- 1 | #ifndef RUBY_JSONNET_RUBY_JSONNET_H_ 2 | #define RUBY_JSONNET_RUBY_JSONNET_H_ 3 | 4 | #include 5 | #include 6 | 7 | extern const rb_data_type_t jsonnet_vm_type; 8 | 9 | struct native_callback_ctx { 10 | VALUE callback; 11 | long arity; 12 | VALUE vm; 13 | }; 14 | 15 | struct jsonnet_vm_wrap { 16 | struct JsonnetVm *vm; 17 | 18 | VALUE import_callback; 19 | struct { 20 | long len; 21 | struct native_callback_ctx **contexts; 22 | } native_callbacks; 23 | }; 24 | 25 | void rubyjsonnet_init_vm(VALUE mod); 26 | void rubyjsonnet_init_callbacks(VALUE cVM); 27 | void rubyjsonnet_init_helpers(VALUE mod); 28 | 29 | struct jsonnet_vm_wrap *rubyjsonnet_obj_to_vm(VALUE vm); 30 | 31 | VALUE rubyjsonnet_json_to_obj(struct JsonnetVm *vm, const struct JsonnetJsonValue *value); 32 | struct JsonnetJsonValue *rubyjsonnet_obj_to_json(struct JsonnetVm *vm, VALUE obj, int *success); 33 | 34 | rb_encoding *rubyjsonnet_assert_asciicompat(VALUE str); 35 | char *rubyjsonnet_str_to_cstr(struct JsonnetVm *vm, VALUE str); 36 | char *rubyjsonnet_str_to_ptr(struct JsonnetVm *vm, VALUE str, size_t *buflen); 37 | VALUE rubyjsonnet_format_exception(VALUE exc); 38 | int rubyjsonnet_jump_tag(const char *exc_mesg); 39 | 40 | #endif /* RUBY_JSONNET_RUBY_JSONNET_H_ */ 41 | -------------------------------------------------------------------------------- /ext/jsonnet/vm.c: -------------------------------------------------------------------------------- 1 | #include 2 | #ifdef HAVE_LIBJSONNET_FMT_H 3 | # include 4 | #endif 5 | #include 6 | #include 7 | 8 | #include "ruby_jsonnet.h" 9 | 10 | #ifndef NORETURN 11 | # define NORETURN(x) x 12 | #endif 13 | 14 | /* 15 | * defines the core part of Jsonnet::VM 16 | */ 17 | 18 | /* 19 | * Jsonnet evaluator 20 | * 21 | * call-seq: 22 | * Jsonnet::VM 23 | */ 24 | static VALUE cVM; 25 | 26 | /* 27 | * Raised on evaluation errors in a Jsonnet VM. 28 | */ 29 | static VALUE eEvaluationError; 30 | static VALUE eFormatError; 31 | 32 | static void raise_eval_error(struct JsonnetVm *vm, char *msg, rb_encoding *enc); 33 | static void raise_format_error(struct JsonnetVm *vm, char *msg, rb_encoding *enc); 34 | static VALUE str_new_json(struct JsonnetVm *vm, char *json, rb_encoding *enc); 35 | static VALUE fileset_new(struct JsonnetVm *vm, char *buf, rb_encoding *enc); 36 | 37 | static void vm_free(void *ptr); 38 | static void vm_mark(void *ptr); 39 | 40 | const rb_data_type_t jsonnet_vm_type = { 41 | "JsonnetVm", 42 | { 43 | /* dmark = */ vm_mark, 44 | /* dfree = */ vm_free, 45 | /* dsize = */ 0, 46 | }, 47 | /* parent = */ 0, 48 | /* data = */ 0, 49 | /* flags = */ RUBY_TYPED_FREE_IMMEDIATELY, 50 | }; 51 | 52 | struct jsonnet_vm_wrap * 53 | rubyjsonnet_obj_to_vm(VALUE wrap) 54 | { 55 | struct jsonnet_vm_wrap *vm; 56 | TypedData_Get_Struct(wrap, struct jsonnet_vm_wrap, &jsonnet_vm_type, vm); 57 | 58 | return vm; 59 | } 60 | 61 | static VALUE 62 | vm_s_allocate(VALUE klass) 63 | { 64 | struct jsonnet_vm_wrap *vm; 65 | VALUE self = TypedData_Make_Struct(klass, struct jsonnet_vm_wrap, &jsonnet_vm_type, vm); 66 | vm->vm = jsonnet_make(); 67 | vm->import_callback = Qnil; 68 | vm->native_callbacks.len = 0; 69 | vm->native_callbacks.contexts = NULL; 70 | 71 | return self; 72 | } 73 | 74 | static void 75 | vm_free(void *ptr) 76 | { 77 | int i; 78 | struct jsonnet_vm_wrap *vm = (struct jsonnet_vm_wrap *)ptr; 79 | jsonnet_destroy(vm->vm); 80 | 81 | for (i = 0; i < vm->native_callbacks.len; ++i) { 82 | struct native_callback_ctx *ctx = vm->native_callbacks.contexts[i]; 83 | xfree(ctx); 84 | } 85 | xfree(vm->native_callbacks.contexts); 86 | xfree(vm); 87 | } 88 | 89 | static void 90 | vm_mark(void *ptr) 91 | { 92 | int i; 93 | struct jsonnet_vm_wrap *vm = (struct jsonnet_vm_wrap *)ptr; 94 | 95 | rb_gc_mark(vm->import_callback); 96 | for (i = 0; i < vm->native_callbacks.len; ++i) { 97 | rb_gc_mark(vm->native_callbacks.contexts[i]->callback); 98 | } 99 | } 100 | 101 | static VALUE 102 | vm_evaluate_file(VALUE self, VALUE fname, VALUE encoding, VALUE multi_p) 103 | { 104 | int error; 105 | char *result; 106 | rb_encoding *const enc = rb_to_encoding(encoding); 107 | struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); 108 | 109 | FilePathValue(fname); 110 | if (RTEST(multi_p)) { 111 | result = jsonnet_evaluate_file_multi(vm->vm, StringValueCStr(fname), &error); 112 | } else { 113 | result = jsonnet_evaluate_file(vm->vm, StringValueCStr(fname), &error); 114 | } 115 | 116 | if (error) { 117 | raise_eval_error(vm->vm, result, rb_enc_get(fname)); 118 | } 119 | return RTEST(multi_p) ? fileset_new(vm->vm, result, enc) : str_new_json(vm->vm, result, enc); 120 | } 121 | 122 | static VALUE 123 | vm_evaluate(VALUE self, VALUE snippet, VALUE fname, VALUE multi_p) 124 | { 125 | int error; 126 | char *result; 127 | struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); 128 | 129 | rb_encoding *enc = rubyjsonnet_assert_asciicompat(StringValue(snippet)); 130 | FilePathValue(fname); 131 | if (RTEST(multi_p)) { 132 | result = jsonnet_evaluate_snippet_multi(vm->vm, StringValueCStr(fname), 133 | StringValueCStr(snippet), &error); 134 | } else { 135 | result = jsonnet_evaluate_snippet(vm->vm, StringValueCStr(fname), StringValueCStr(snippet), 136 | &error); 137 | } 138 | 139 | if (error) { 140 | raise_eval_error(vm->vm, result, rb_enc_get(fname)); 141 | } 142 | return RTEST(multi_p) ? fileset_new(vm->vm, result, enc) : str_new_json(vm->vm, result, enc); 143 | } 144 | 145 | #define vm_bind_variable(type, self, key, val) \ 146 | do { \ 147 | struct jsonnet_vm_wrap *vm; \ 148 | \ 149 | rubyjsonnet_assert_asciicompat(StringValue(key)); \ 150 | rubyjsonnet_assert_asciicompat(StringValue(val)); \ 151 | TypedData_Get_Struct(self, struct jsonnet_vm_wrap, &jsonnet_vm_type, vm); \ 152 | jsonnet_##type(vm->vm, StringValueCStr(key), StringValueCStr(val)); \ 153 | } while (0) 154 | 155 | /* 156 | * Binds an external variable to a value. 157 | * @param [String] key name of the variable 158 | * @param [String] val the value 159 | */ 160 | static VALUE 161 | vm_ext_var(VALUE self, VALUE key, VALUE val) 162 | { 163 | vm_bind_variable(ext_var, self, key, val); 164 | return Qnil; 165 | } 166 | 167 | /* 168 | * Binds an external variable to a code fragment. 169 | * @param [String] key name of the variable 170 | * @param [String] code Jsonnet expression 171 | */ 172 | static VALUE 173 | vm_ext_code(VALUE self, VALUE key, VALUE code) 174 | { 175 | vm_bind_variable(ext_code, self, key, code); 176 | return Qnil; 177 | } 178 | 179 | /* 180 | * Binds a top-level argument to a value. 181 | * @param [String] key name of the variable 182 | * @param [String] val the value 183 | */ 184 | static VALUE 185 | vm_tla_var(VALUE self, VALUE key, VALUE val) 186 | { 187 | vm_bind_variable(tla_var, self, key, val); 188 | return Qnil; 189 | } 190 | 191 | /* 192 | * Binds a top-level argument to a code fragment. 193 | * @param [String] key name of the variable 194 | * @param [String] code Jsonnet expression 195 | */ 196 | static VALUE 197 | vm_tla_code(VALUE self, VALUE key, VALUE code) 198 | { 199 | vm_bind_variable(tla_code, self, key, code); 200 | return Qnil; 201 | } 202 | 203 | /* 204 | * Adds library search paths 205 | */ 206 | static VALUE 207 | vm_jpath_add_m(int argc, const VALUE *argv, VALUE self) 208 | { 209 | int i; 210 | struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); 211 | 212 | for (i = 0; i < argc; ++i) { 213 | VALUE jpath = argv[i]; 214 | FilePathValue(jpath); 215 | jsonnet_jpath_add(vm->vm, StringValueCStr(jpath)); 216 | } 217 | return Qnil; 218 | } 219 | 220 | static VALUE 221 | vm_set_max_stack(VALUE self, VALUE val) 222 | { 223 | struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); 224 | jsonnet_max_stack(vm->vm, NUM2UINT(val)); 225 | return Qnil; 226 | } 227 | 228 | static VALUE 229 | vm_set_gc_min_objects(VALUE self, VALUE val) 230 | { 231 | struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); 232 | jsonnet_gc_min_objects(vm->vm, NUM2UINT(val)); 233 | return Qnil; 234 | } 235 | 236 | static VALUE 237 | vm_set_gc_growth_trigger(VALUE self, VALUE val) 238 | { 239 | struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); 240 | jsonnet_gc_growth_trigger(vm->vm, NUM2DBL(val)); 241 | return Qnil; 242 | } 243 | 244 | /* 245 | * Let #evaluate and #evaluate_file return a raw String instead of JSON-encoded string if val is 246 | * true 247 | * @param [Boolean] val 248 | */ 249 | static VALUE 250 | vm_set_string_output(VALUE self, VALUE val) 251 | { 252 | struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); 253 | jsonnet_string_output(vm->vm, RTEST(val)); 254 | return Qnil; 255 | } 256 | 257 | static VALUE 258 | vm_set_max_trace(VALUE self, VALUE val) 259 | { 260 | struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); 261 | jsonnet_max_trace(vm->vm, NUM2UINT(val)); 262 | return Qnil; 263 | } 264 | 265 | static VALUE 266 | vm_set_fmt_indent(VALUE self, VALUE val) 267 | { 268 | struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); 269 | jsonnet_fmt_indent(vm->vm, NUM2INT(val)); 270 | return val; 271 | } 272 | 273 | static VALUE 274 | vm_set_fmt_max_blank_lines(VALUE self, VALUE val) 275 | { 276 | struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); 277 | jsonnet_fmt_max_blank_lines(vm->vm, NUM2INT(val)); 278 | return val; 279 | } 280 | 281 | static VALUE 282 | vm_set_fmt_string(VALUE self, VALUE str) 283 | { 284 | const char *ptr; 285 | struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); 286 | StringValue(str); 287 | if (RSTRING_LEN(str) != 1) { 288 | rb_raise(rb_eArgError, "fmt_string must have a length of 1"); 289 | } 290 | ptr = RSTRING_PTR(str); 291 | switch (*ptr) { 292 | case 'd': 293 | case 's': 294 | case 'l': 295 | jsonnet_fmt_string(vm->vm, *ptr); 296 | return str; 297 | default: 298 | rb_raise(rb_eArgError, "fmt_string only accepts 'd', 's', or 'l'"); 299 | } 300 | } 301 | 302 | static VALUE 303 | vm_set_fmt_comment(VALUE self, VALUE str) 304 | { 305 | const char *ptr; 306 | struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); 307 | StringValue(str); 308 | if (RSTRING_LEN(str) != 1) { 309 | rb_raise(rb_eArgError, "fmt_comment must have a length of 1"); 310 | } 311 | ptr = RSTRING_PTR(str); 312 | switch (*ptr) { 313 | case 'h': 314 | case 's': 315 | case 'l': 316 | jsonnet_fmt_comment(vm->vm, *ptr); 317 | return str; 318 | default: 319 | rb_raise(rb_eArgError, "fmt_comment only accepts 'h', 's', or 'l'"); 320 | } 321 | } 322 | 323 | static VALUE 324 | vm_set_fmt_pad_arrays(VALUE self, VALUE val) 325 | { 326 | struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); 327 | jsonnet_fmt_pad_objects(vm->vm, RTEST(val) ? 1 : 0); 328 | return val; 329 | } 330 | 331 | static VALUE 332 | vm_set_fmt_pad_objects(VALUE self, VALUE val) 333 | { 334 | struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); 335 | jsonnet_fmt_pad_objects(vm->vm, RTEST(val) ? 1 : 0); 336 | return val; 337 | } 338 | 339 | static VALUE 340 | vm_set_fmt_pretty_field_names(VALUE self, VALUE val) 341 | { 342 | struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); 343 | jsonnet_fmt_pretty_field_names(vm->vm, RTEST(val) ? 1 : 0); 344 | return val; 345 | } 346 | 347 | static VALUE 348 | vm_set_fmt_sort_imports(VALUE self, VALUE val) 349 | { 350 | struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); 351 | jsonnet_fmt_sort_imports(vm->vm, RTEST(val) ? 1 : 0); 352 | return val; 353 | } 354 | 355 | static VALUE 356 | vm_fmt_file(VALUE self, VALUE fname, VALUE encoding) 357 | { 358 | int error; 359 | char *result; 360 | rb_encoding *const enc = rb_to_encoding(encoding); 361 | struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); 362 | 363 | FilePathValue(fname); 364 | result = jsonnet_fmt_file(vm->vm, StringValueCStr(fname), &error); 365 | if (error) { 366 | raise_format_error(vm->vm, result, rb_enc_get(fname)); 367 | } 368 | return str_new_json(vm->vm, result, enc); 369 | } 370 | 371 | static VALUE 372 | vm_fmt_snippet(VALUE self, VALUE snippet, VALUE fname) 373 | { 374 | int error; 375 | char *result; 376 | struct jsonnet_vm_wrap *vm = rubyjsonnet_obj_to_vm(self); 377 | 378 | rb_encoding *enc = rubyjsonnet_assert_asciicompat(StringValue(snippet)); 379 | FilePathValue(fname); 380 | result = jsonnet_fmt_snippet(vm->vm, StringValueCStr(fname), StringValueCStr(snippet), &error); 381 | if (error) { 382 | raise_format_error(vm->vm, result, rb_enc_get(fname)); 383 | } 384 | return str_new_json(vm->vm, result, enc); 385 | } 386 | 387 | void 388 | rubyjsonnet_init_vm(VALUE mJsonnet) 389 | { 390 | cVM = rb_define_class_under(mJsonnet, "VM", rb_cObject); 391 | rb_define_alloc_func(cVM, vm_s_allocate); 392 | rb_define_private_method(cVM, "eval_file", vm_evaluate_file, 3); 393 | rb_define_private_method(cVM, "eval_snippet", vm_evaluate, 3); 394 | rb_define_private_method(cVM, "fmt_file", vm_fmt_file, 2); 395 | rb_define_private_method(cVM, "fmt_snippet", vm_fmt_snippet, 2); 396 | rb_define_method(cVM, "ext_var", vm_ext_var, 2); 397 | rb_define_method(cVM, "ext_code", vm_ext_code, 2); 398 | rb_define_method(cVM, "tla_var", vm_tla_var, 2); 399 | rb_define_method(cVM, "tla_code", vm_tla_code, 2); 400 | rb_define_method(cVM, "jpath_add", vm_jpath_add_m, -1); 401 | rb_define_method(cVM, "max_stack=", vm_set_max_stack, 1); 402 | rb_define_method(cVM, "gc_min_objects=", vm_set_gc_min_objects, 1); 403 | rb_define_method(cVM, "gc_growth_trigger=", vm_set_gc_growth_trigger, 1); 404 | rb_define_method(cVM, "string_output=", vm_set_string_output, 1); 405 | rb_define_method(cVM, "max_trace=", vm_set_max_trace, 1); 406 | rb_define_method(cVM, "fmt_indent=", vm_set_fmt_indent, 1); 407 | rb_define_method(cVM, "fmt_max_blank_lines=", vm_set_fmt_max_blank_lines, 1); 408 | rb_define_method(cVM, "fmt_string=", vm_set_fmt_string, 1); 409 | rb_define_method(cVM, "fmt_comment=", vm_set_fmt_comment, 1); 410 | rb_define_method(cVM, "fmt_pad_arrays=", vm_set_fmt_pad_arrays, 1); 411 | rb_define_method(cVM, "fmt_pad_objects=", vm_set_fmt_pad_objects, 1); 412 | rb_define_method(cVM, "fmt_pretty_field_names=", vm_set_fmt_pretty_field_names, 1); 413 | rb_define_method(cVM, "fmt_sort_imports=", vm_set_fmt_sort_imports, 1); 414 | 415 | rb_define_const(mJsonnet, "STRING_STYLE_DOUBLE", rb_str_new_cstr("d")); 416 | rb_define_const(mJsonnet, "STRING_STYLE_SINGLE", rb_str_new_cstr("s")); 417 | rb_define_const(mJsonnet, "STRING_STYLE_LEAVE", rb_str_new_cstr("l")); 418 | rb_define_const(mJsonnet, "COMMENT_STYLE_HASH", rb_str_new_cstr("h")); 419 | rb_define_const(mJsonnet, "COMMENT_STYLE_SLASH", rb_str_new_cstr("s")); 420 | rb_define_const(mJsonnet, "COMMENT_STYLE_LEAVE", rb_str_new_cstr("l")); 421 | 422 | rubyjsonnet_init_callbacks(cVM); 423 | 424 | eEvaluationError = rb_define_class_under(mJsonnet, "EvaluationError", rb_eRuntimeError); 425 | eFormatError = rb_define_class_under(mJsonnet, "FormatError", rb_eRuntimeError); 426 | } 427 | 428 | static void 429 | NORETURN(raise_error)(VALUE exception_class, struct JsonnetVm *vm, char *msg, rb_encoding *enc) 430 | { 431 | VALUE ex; 432 | const int state = rubyjsonnet_jump_tag(msg); 433 | if (state) { 434 | /* 435 | * This is not actually an exception but another type of long jump 436 | * with the state, temporarily caught by rescue_callback(). 437 | */ 438 | jsonnet_realloc(vm, msg, 0); 439 | rb_jump_tag(state); 440 | } 441 | 442 | ex = rb_exc_new3(exception_class, rb_enc_str_new_cstr(msg, enc)); 443 | jsonnet_realloc(vm, msg, 0); 444 | rb_exc_raise(ex); 445 | } 446 | 447 | /** 448 | * raises an EvaluationError whose message is \c msg. 449 | * @param[in] vm a JsonnetVM 450 | * @param[in] msg must be a NUL-terminated string returned by \c vm. 451 | * @return never returns 452 | * @throw EvaluationError 453 | * @sa rescue_callback 454 | */ 455 | static void 456 | NORETURN(raise_eval_error)(struct JsonnetVm *vm, char *msg, rb_encoding *enc) 457 | { 458 | raise_error(eEvaluationError, vm, msg, enc); 459 | } 460 | 461 | static void 462 | NORETURN(raise_format_error)(struct JsonnetVm *vm, char *msg, rb_encoding *enc) 463 | { 464 | raise_error(eFormatError, vm, msg, enc); 465 | } 466 | 467 | /** 468 | * Returns a String whose contents is equal to \c json. 469 | * It automatically frees \c json just after constructing the return value. 470 | * 471 | * @param[in] vm a JsonnetVM 472 | * @param[in] json must be a NUL-terminated string returned by \c vm. 473 | * @return Ruby string equal to \c json. 474 | */ 475 | static VALUE 476 | str_new_json(struct JsonnetVm *vm, char *json, rb_encoding *enc) 477 | { 478 | VALUE str = rb_enc_str_new_cstr(json, enc); 479 | jsonnet_realloc(vm, json, 0); 480 | return str; 481 | } 482 | 483 | /** 484 | * Returns a Hash, whose keys are file names in the multi-mode of Jsonnet, 485 | * and whose values are corresponding JSON values. 486 | * It automatically frees \c json just after constructing the return value. 487 | * 488 | * @param[in] vm a JsonnetVM 489 | * @param[in] buf NUL-separated and double-NUL-terminated sequence of strings returned by \c vm. 490 | * @return Hash 491 | */ 492 | static VALUE 493 | fileset_new(struct JsonnetVm *vm, char *buf, rb_encoding *enc) 494 | { 495 | VALUE fileset = rb_hash_new(); 496 | char *ptr, *json; 497 | for (ptr = buf; *ptr; ptr = json + strlen(json) + 1) { 498 | json = ptr + strlen(ptr) + 1; 499 | if (!*json) { 500 | VALUE ex = rb_exc_new3(eEvaluationError, 501 | rb_enc_sprintf(enc, "output file %s without body", ptr)); 502 | jsonnet_realloc(vm, buf, 0); 503 | rb_exc_raise(ex); 504 | } 505 | 506 | rb_hash_aset(fileset, rb_enc_str_new_cstr(ptr, enc), rb_enc_str_new_cstr(json, enc)); 507 | } 508 | jsonnet_realloc(vm, buf, 0); 509 | return fileset; 510 | } 511 | -------------------------------------------------------------------------------- /jsonnet.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'jsonnet/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "jsonnet" 8 | spec.version = Jsonnet::VERSION 9 | spec.authors = ["Yuki Yugui Sonoda"] 10 | spec.email = ["yugui@yugui.jp"] 11 | spec.summary = %q{Jsonnet library} 12 | spec.description = %q{Wraps the official C++ implementation of Jsonnet} 13 | spec.homepage = "" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.extensions = ['ext/jsonnet/extconf.rb'] 19 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 20 | spec.require_paths = ["lib"] 21 | 22 | spec.add_runtime_dependency "mini_portile2", ">= 2.2.0" 23 | 24 | spec.add_development_dependency "bundler", ">= 1.7" 25 | spec.add_development_dependency "rake", ">= 10.0" 26 | spec.add_development_dependency "test-unit", ">= 3.1.3" 27 | spec.add_development_dependency "rake-compiler", ">= 0.9.5" 28 | end 29 | -------------------------------------------------------------------------------- /lib/jsonnet.rb: -------------------------------------------------------------------------------- 1 | require "jsonnet/version" 2 | require "jsonnet/vm" 3 | require "json" 4 | 5 | module Jsonnet 6 | module_function 7 | 8 | ## 9 | # Evaluates a string of Jsonnet and returns a hash of the resulting JSON 10 | # 11 | # @param [String] jsonnet Jsonnet source string, ideally in UTF-8 encoding 12 | # @param [Hash] jsonnet_options A hash of options to for Jsonnet::VM. 13 | # Available options are: filename, multi, 14 | # import_callback, gc_growth_triger, 15 | # gc_min_objects, max_stack, max_trace 16 | # @param [Hash] json_options Options supported by {JSON.parse}[http://www.rubydoc.info/github/flori/json/JSON#parse-class_method] 17 | # @return [Hash] The JSON representation as a hash 18 | # @raise [UnsupportedOptionError] Raised when an option passed is unsupported by Jsonnet::VM 19 | # 20 | # @note This method runs Jsonnet::VM#evaluate and runs the string 21 | # output through {JSON.parse}[http://www.rubydoc.info/github/flori/json/JSON#parse-class_method] 22 | # so those should be looked at for furhter details 23 | def evaluate(jsonnet, jsonnet_options: {}, json_options: {}) 24 | output = VM.evaluate(jsonnet, jsonnet_options) 25 | JSON.parse(output, json_options) 26 | end 27 | 28 | ## 29 | # Loads a Jsonnet file and returns a hash of the resulting JSON 30 | # 31 | # @param [String] path path to the jsonnet file 32 | # @param [Hash] jsonnet_options A hash of options to for Jsonnet::VM. 33 | # Available options are: encoding, multi, 34 | # import_callback, gc_growth_triger, 35 | # gc_min_objects, max_stack, max_trace 36 | # @param [Hash] json_options Options supported by {JSON.parse}[http://www.rubydoc.info/github/flori/json/JSON#parse-class_method] 37 | # @return [Hash] The JSON representation as a hash 38 | # @raise [UnsupportedOptionError] Raised when an option passed is unsupported by Jsonnet::VM 39 | # 40 | # @note This method runs Jsonnet::VM#evaluate_file and runs the string 41 | # output through {JSON.parse}[http://www.rubydoc.info/github/flori/json/JSON#parse-class_method] 42 | # so those should be looked at for furhter details 43 | def load(path, jsonnet_options: {}, json_options: {}) 44 | output = VM.evaluate_file(path, jsonnet_options) 45 | JSON.parse(output, json_options) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/jsonnet/version.rb: -------------------------------------------------------------------------------- 1 | module Jsonnet 2 | VERSION = "0.6.0" 3 | end 4 | -------------------------------------------------------------------------------- /lib/jsonnet/vm.rb: -------------------------------------------------------------------------------- 1 | require "jsonnet/jsonnet_wrap" 2 | 3 | module Jsonnet 4 | class VM 5 | class << self 6 | ## 7 | # Convenient method to evaluate a Jsonnet snippet. 8 | # 9 | # It implicitly instantiates a VM and then evaluate Jsonnet with the VM. 10 | # 11 | # @param snippet [String] Jsonnet source string. 12 | # @param options [Hash] options to {.new} or options to {#evaluate} 13 | # @return [String] 14 | # @see #evaluate 15 | def evaluate(snippet, options = {}) 16 | snippet_check = ->(key, value) { key.to_s.match(/^filename|multi$/) } 17 | snippet_options = options.select(&snippet_check) 18 | vm_options = options.reject(&snippet_check) 19 | new(vm_options).evaluate(snippet, **snippet_options) 20 | end 21 | 22 | ## 23 | # Convenient method to evaluate a Jsonnet file. 24 | # 25 | # It implicitly instantiates a VM and then evaluates Jsonnet with the VM. 26 | # 27 | # @param filename [String] Jsonnet source file. 28 | # @param options [Hash] options to {.new} or options to {#evaluate_file} 29 | # @return [String] 30 | # @see #evaluate_file 31 | def evaluate_file(filename, options = {}) 32 | file_check = ->(key, value) { key.to_s.match(/^encoding|multi$/) } 33 | file_options = options.select(&file_check) 34 | vm_options = options.reject(&file_check) 35 | new(vm_options).evaluate_file(filename, **file_options) 36 | end 37 | end 38 | 39 | ## 40 | # initializes a new VM with the given configuration. 41 | # 42 | # @param [Hash] options a mapping from option names to their values. 43 | # It can have names of writable attributes in VM class as keys. 44 | # @return [VM] the VM. 45 | def initialize(options = {}) 46 | options.each do |key, value| 47 | method = "#{key}=" 48 | if respond_to?(method) 49 | public_send(method, value) 50 | else 51 | raise UnsupportedOptionError.new("Jsonnet VM does not support #{key} option") 52 | end 53 | end 54 | self 55 | end 56 | 57 | ## 58 | # Evaluates Jsonnet source. 59 | # 60 | # @param [String] jsonnet Jsonnet source string. 61 | # Must be encoded in an ASCII-compatible encoding. 62 | # @param [String] filename filename of the source. Used in stacktrace. 63 | # @param [Boolean] multi enables multi-mode 64 | # @return [String] a JSON representation of the evaluation result 65 | # @raise [EvaluationError] raised when the evaluation results an error. 66 | # @raise [UnsupportedEncodingError] raised when the encoding of jsonnet 67 | # is not ASCII-compatible. 68 | # @note It is recommended to encode the source string in UTF-8 because 69 | # Jsonnet expects it is ASCII-compatible, the result JSON string 70 | # shall be UTF-{8,16,32} according to RFC 7159 thus the only 71 | # intersection between the requirements is UTF-8. 72 | def evaluate(jsonnet, filename: "(jsonnet)", multi: false) 73 | eval_snippet(jsonnet, filename, multi) 74 | end 75 | 76 | ## 77 | # Evaluates Jsonnet file. 78 | # 79 | # @param [String] filename filename of a Jsonnet source file. 80 | # @param [Boolean] multi enables multi-mode 81 | # @return [String] a JSON representation of the evaluation result 82 | # @raise [EvaluationError] raised when the evaluation results an error. 83 | # @note It is recommended to encode the source file in UTF-8 because 84 | # Jsonnet expects it is ASCII-compatible, the result JSON string 85 | # shall be UTF-{8,16,32} according to RFC 7159 thus the only 86 | # intersection between the requirements is UTF-8. 87 | def evaluate_file(filename, encoding: Encoding.default_external, multi: false) 88 | eval_file(filename, encoding, multi) 89 | end 90 | 91 | ## 92 | # Format Jsonnet file. 93 | # 94 | # @param [String] filename filename of a Jsonnet source file. 95 | # @return [String] a formatted Jsonnet representation 96 | # @raise [FormatError] raised when the formatting results an error. 97 | def format_file(filename, encoding: Encoding.default_external) 98 | fmt_file(filename, encoding) 99 | end 100 | 101 | ## 102 | # Format Jsonnet snippet. 103 | # 104 | # @param [String] jsonnet Jsonnet source string. Must be encoded in ASCII-compatible encoding. 105 | # @param [String] filename filename of the source. Used in stacktrace. 106 | # @return [String] a formatted Jsonnet representation 107 | # @raise [FormatError] raised when the formatting results an error. 108 | # @raise [UnsupportedEncodingError] raised when the encoding of jsonnt is not ASCII-compatible. 109 | def format(jsonnet, filename: "(jsonnet)") 110 | fmt_snippet(jsonnet, filename) 111 | end 112 | 113 | ## 114 | # Lets the given block handle "import" expression of Jsonnet. 115 | # @yieldparam [String] base base path to resolve "rel" from. 116 | # @yieldparam [String] rel a relative or absolute path to the file to be imported 117 | # @yieldreturn [Array] a pair of the content of the imported file and 118 | # its path. 119 | def handle_import(&block) 120 | if block.nil? 121 | raise ArgumentError, 'handle_import requires a block' 122 | end 123 | self.import_callback = to_method(block) 124 | nil 125 | end 126 | 127 | ## 128 | # Define a function (native extension) in the VM and let the given block 129 | # handle the invocation of the function. 130 | # 131 | # @param name [Symbol|String] name of the function. 132 | # Must be a valid identifier in Jsonnet. 133 | # @param body [#to_proc] body of the function. 134 | # @yield calls the given block instead of `body` if `body` is `nil` 135 | # 136 | # @note Currently it cannot define keyword or optional paramters in Jsonnet. 137 | # Also all the positional optional parameters of the body are interpreted 138 | # as required parameters. And the body cannot have keyword, rest or 139 | # keyword rest paramters. 140 | def define_function(name, body = nil, &block) 141 | body = body ? body.to_proc : block 142 | if body.nil? 143 | raise ArgumentError, 'define_function requires a body argument or a block' 144 | end 145 | params = body.parameters.map.with_index do |(type, name), i| 146 | raise ArgumentError, "rest or keyword parameters are not allowed: #{type}" \ 147 | unless [:req, :opt].include? type 148 | 149 | name || "p#{i}" 150 | end 151 | 152 | register_native_callback(name.to_sym, to_method(body), params); 153 | end 154 | 155 | private 156 | # Wraps the function body with a method so that `break` and `return` 157 | # behave like `return` as they do in a body of Module#define_method. 158 | def to_method(body) 159 | mod = Module.new { 160 | define_method(:dummy, body) 161 | } 162 | mod.instance_method(:dummy).bind(body.binding.receiver) 163 | end 164 | 165 | class UnsupportedOptionError < RuntimeError; end 166 | end 167 | end 168 | -------------------------------------------------------------------------------- /test/fixtures/jpath.libsonnet: -------------------------------------------------------------------------------- 1 | { 2 | a: 1, 3 | } 4 | -------------------------------------------------------------------------------- /test/test_jsonnet.rb: -------------------------------------------------------------------------------- 1 | require 'jsonnet' 2 | 3 | require 'tempfile' 4 | require 'test/unit' 5 | 6 | class TestJsonnet < Test::Unit::TestCase 7 | test 'libversion returns a String' do 8 | assert_kind_of String, Jsonnet.libversion 9 | end 10 | 11 | test 'Jsonnet.evaluate returns a JSON parsed result' do 12 | result = Jsonnet.evaluate('{ foo: "bar" }') 13 | assert_equal result, { "foo" => "bar" } 14 | end 15 | 16 | test 'Jsonnet.evaluate can accept options for JSON' do 17 | result = Jsonnet.evaluate('{ foo: "bar" }', json_options: { symbolize_names: true }) 18 | assert_equal result, { foo: "bar" } 19 | end 20 | 21 | test 'Jsonnet.evaluate can accept options for Jsonnet VM' do 22 | result = Jsonnet.evaluate( 23 | 'import "imported.jsonnet"', 24 | jsonnet_options: { 25 | import_callback: ->(_base, _rel) do 26 | return ['{ foo: "bar" }', 'imported'] 27 | end 28 | } 29 | ) 30 | assert_equal result, { "foo" => "bar" } 31 | end 32 | 33 | test 'Jsonnet.load returns a JSON parsed result' do 34 | result = Jsonnet.load(example_jsonnet_file.path) 35 | assert_equal result, { "foo1" => 1 } 36 | end 37 | 38 | private 39 | 40 | def example_jsonnet_file 41 | Tempfile.open("example.jsonnet") do |f| 42 | f.write %< 43 | local myvar = 1; 44 | { 45 | ["foo" + myvar]: myvar, 46 | } 47 | > 48 | f 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/test_vm.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | require 'jsonnet' 3 | 4 | require 'json' 5 | require 'tempfile' 6 | require 'test/unit' 7 | 8 | class TestVM < Test::Unit::TestCase 9 | test 'Jsonnet::VM#evaluate_file evaluates file' do 10 | vm = Jsonnet::VM.new 11 | with_example_file(%< 12 | local myvar = 1; 13 | { 14 | ["foo" + myvar]: myvar, 15 | } 16 | >) {|fname| 17 | result = vm.evaluate_file(fname) 18 | assert_equal JSON.parse(<<-EOS), JSON.parse(result) 19 | {"foo1": 1} 20 | EOS 21 | } 22 | end 23 | 24 | test 'Jsonnet::VM#evaluate_file raises an EvaluationError on error' do 25 | vm = Jsonnet::VM.new 26 | with_example_file(%< 27 | { 28 | // unbound variable 29 | ["foo" + myvar]: myvar, 30 | } 31 | >) {|fname| 32 | assert_raise(Jsonnet::EvaluationError) do 33 | vm.evaluate_file(fname) 34 | end 35 | } 36 | end 37 | 38 | test "Jsonnet::VM#evaluate_file returns the same encoding as source" do 39 | vm = Jsonnet::VM.new 40 | with_example_file(%Q{ ["テスト"] }.encode(Encoding::EUC_JP)) {|fname| 41 | result = vm.evaluate_file(fname, encoding: Encoding::EUC_JP) 42 | assert_equal Encoding::EUC_JP, result.encoding 43 | } 44 | end 45 | 46 | test "Jsonnet::VM#evaluate_file raises an error in the encoding of filename" do 47 | vm = Jsonnet::VM.new 48 | begin 49 | with_example_file(%q{ ["unterminated string }) {|fname| 50 | vm.evaluate_file(fname.encode(Encoding::SJIS)) 51 | } 52 | rescue Jsonnet::EvaluationError => e 53 | assert_equal Encoding::SJIS, e.message.encoding 54 | end 55 | end 56 | 57 | test 'Jsonnet::VM#evaluate evaluates snippet' do 58 | vm = Jsonnet::VM.new 59 | result = vm.evaluate(<<-EOS, filename: 'example.snippet') 60 | local myvar = 1; 61 | { 62 | ["foo" + myvar]: myvar, 63 | } 64 | EOS 65 | 66 | assert_equal JSON.parse(<<-EOS), JSON.parse(result) 67 | {"foo1": 1} 68 | EOS 69 | end 70 | 71 | test 'Jsonnet::VM#evaluate can be called without filename' do 72 | vm = Jsonnet::VM.new 73 | result = vm.evaluate(<<-EOS) 74 | local myvar = 1; 75 | { 76 | ["foo" + myvar]: myvar, 77 | } 78 | EOS 79 | 80 | assert_equal JSON.parse(<<-EOS), JSON.parse(result) 81 | {"foo1": 1} 82 | EOS 83 | end 84 | 85 | test 'Jsonnet::VM#evaluate raises an EvaluationError on error' do 86 | vm = Jsonnet::VM.new 87 | assert_raise(Jsonnet::EvaluationError) do 88 | vm.evaluate(<<-EOS, filename: 'example.snippet') 89 | { 90 | // unbound variable 91 | ["foo" + myvar]: myvar, 92 | } 93 | EOS 94 | end 95 | end 96 | 97 | test "Jsonnet::VM#evaluate returns the same encoding as source" do 98 | vm = Jsonnet::VM.new 99 | result = vm.evaluate(%Q{ ["テスト"] }.encode(Encoding::EUC_JP)) 100 | assert_equal Encoding::EUC_JP, result.encoding 101 | end 102 | 103 | test "Jsonnet::VM#evaluate raises an error in the encoding of filename" do 104 | vm = Jsonnet::VM.new 105 | begin 106 | vm.evaluate(%Q{ ["unterminated string }, filename: "テスト.json".encode(Encoding::SJIS)) 107 | rescue Jsonnet::EvaluationError => e 108 | assert_equal Encoding::SJIS, e.message.encoding 109 | end 110 | end 111 | 112 | test "Jsonnet::VM#ext_var binds a variable to a string value" do 113 | vm = Jsonnet::VM.new 114 | vm.ext_var("var1", "foo") 115 | result = vm.evaluate('[std.extVar("var1")]') 116 | assert_equal JSON.parse('["foo"]'), JSON.parse(result) 117 | end 118 | 119 | test "Jsonnet::VM#ext_code binds a variable to a code fragment" do 120 | vm = Jsonnet::VM.new 121 | vm.ext_code("var1", "{a:1}") 122 | result = vm.evaluate('[std.extVar("var1")]') 123 | assert_equal JSON.parse(<<-EOS), JSON.parse(result) 124 | [ 125 | { 126 | "a": 1 127 | } 128 | ] 129 | EOS 130 | end 131 | 132 | test "Jsonnet::VM#tla_var binds a top-level variable to a string value" do 133 | vm = Jsonnet::VM.new 134 | vm.tla_var("var1", "foo") 135 | result = vm.evaluate('function(var1) [var1, var1]') 136 | assert_equal JSON.parse('["foo", "foo"]'), JSON.parse(result) 137 | end 138 | 139 | test "Jsonnet::VM#tla_var binds a top-level argument to a string value" do 140 | vm = Jsonnet::VM.new 141 | vm.tla_var("var1", "foo") 142 | result = vm.evaluate('function(var1) [var1, var1]') 143 | assert_equal JSON.parse('["foo", "foo"]'), JSON.parse(result) 144 | end 145 | 146 | test "Jsonnet::VM#tla_code binds a top-level argument to a code fragment" do 147 | vm = Jsonnet::VM.new 148 | vm.tla_code("var1", "{a:1}") 149 | result = vm.evaluate('function(var1) [var1, var1]') 150 | assert_equal JSON.parse(<<-EOS), JSON.parse(result) 151 | [ 152 | { 153 | "a": 1 154 | }, 155 | { 156 | "a": 1 157 | } 158 | ] 159 | EOS 160 | end 161 | 162 | test 'Jsonnet::VM#evaluate returns a JSON per filename on multi mode' do 163 | vm = Jsonnet::VM.new 164 | [ 165 | [ "{}", {} ], 166 | [ 167 | %< 168 | local myvar = 1; 169 | { ["foo" + myvar]: [myvar] } 170 | >, 171 | { 172 | 'foo1' => [1], 173 | } 174 | ], 175 | [ 176 | %< 177 | local myvar = 1; 178 | { 179 | ["foo" + myvar]: [myvar], 180 | ["bar" + myvar]: { 181 | ["baz" + (myvar+1)]: myvar+1, 182 | }, 183 | } 184 | >, 185 | { 186 | 'foo1' => [1], 187 | 'bar1' => {'baz2' => 2}, 188 | } 189 | ], 190 | ].each do |jsonnet, expected| 191 | result = vm.evaluate(jsonnet, multi: true) 192 | assert_equal expected.keys.sort, result.keys.sort 193 | expected.each do |fname, value| 194 | assert_not_nil result[fname] 195 | assert_equal value, JSON.parse(result[fname]) 196 | end 197 | end 198 | end 199 | 200 | test 'Jsonnet::VM#evaluate_file returns a JSON per filename on multi mode' do 201 | vm = Jsonnet::VM.new 202 | [ 203 | [ "{}", {} ], 204 | [ 205 | %< 206 | local myvar = 1; 207 | { ["foo" + myvar]: [myvar] } 208 | >, 209 | { 210 | 'foo1' => [1], 211 | } 212 | ], 213 | [ 214 | %< 215 | local myvar = 1; 216 | { 217 | ["foo" + myvar]: [myvar], 218 | ["bar" + myvar]: { 219 | ["baz" + (myvar+1)]: myvar+1, 220 | }, 221 | } 222 | >, 223 | { 224 | 'foo1' => [1], 225 | 'bar1' => {'baz2' => 2}, 226 | } 227 | ], 228 | ].each do |jsonnet, expected| 229 | with_example_file(jsonnet) {|fname| 230 | result = vm.evaluate_file(fname, multi: true) 231 | assert_equal expected.keys.sort, result.keys.sort 232 | expected.each do |fname, value| 233 | assert_not_nil result[fname] 234 | assert_equal value, JSON.parse(result[fname]) 235 | end 236 | } 237 | end 238 | end 239 | 240 | test "Jsonnet::VM responds to max_stack=" do 241 | Jsonnet::VM.new.max_stack = 1 242 | end 243 | 244 | test "Jsonnet::VM responds to gc_min_objects=" do 245 | Jsonnet::VM.new.gc_min_objects = 1 246 | end 247 | 248 | test "Jsonnet::VM responds to gc_growth_trigger=" do 249 | Jsonnet::VM.new.gc_growth_trigger = 1.5 250 | end 251 | 252 | test "Jsonnet::VM responds to max_trace=" do 253 | Jsonnet::VM.new.max_trace = 1 254 | end 255 | 256 | test "Jsonnet::VM#string_output lets the VM output a raw string" do 257 | vm = Jsonnet::VM.new 258 | vm.string_output = true 259 | assert_equal "foo\n", vm.evaluate(%q[ "foo" ]) 260 | vm.string_output = false 261 | assert_equal ["foo"], JSON.parse(vm.evaluate(%q[ ["foo"] ])) 262 | end 263 | 264 | test "Jsonnet::VM#import_callback customizes import file resolution" do 265 | vm = Jsonnet::VM.new 266 | vm.import_callback = ->(base, rel) { 267 | case [base, rel] 268 | when ['/path/to/base/', 'imported1.jsonnet'] 269 | return <<-EOS, '/path/to/imported1/imported1.jsonnet' 270 | (import "imported2.jsonnet") + { 271 | b: 2, 272 | } 273 | EOS 274 | when ['/path/to/imported1/', "imported2.jsonnet"] 275 | return <<-EOS, '/path/to/imported2/imported2.jsonnet' 276 | { a: 1 } 277 | EOS 278 | else 279 | raise Errno::ENOENT, "#{rel} at #{base}" 280 | end 281 | } 282 | result = vm.evaluate(<<-EOS, filename: "/path/to/base/example.jsonnet") 283 | (import "imported1.jsonnet") + { c: 3 } 284 | EOS 285 | 286 | expected = {"a" => 1, "b" => 2, "c" => 3} 287 | assert_equal expected, JSON.parse(result) 288 | end 289 | 290 | test "Jsonnet::VM#import_callback allows NUL char in Jsonnet v0.19 or later" do 291 | return unless Jsonnet.libversion >= "v0.19" 292 | 293 | example_str = "\x0\x1".force_encoding(Encoding::BINARY) 294 | 295 | vm = Jsonnet::VM.new 296 | vm.import_callback = ->(base, rel) { 297 | case [base, rel] 298 | when ['/path/to/base/', 'foo.bin'] 299 | return "\x0\x1".force_encoding(Encoding::BINARY), '/path/to/base/foo.bin' 300 | else 301 | raise Errno::ENOENT, "#{rel} at #{base}" 302 | end 303 | } 304 | result = vm.evaluate(<<-EOS, filename: "/path/to/base/example.jsonnet") 305 | importbin "foo.bin" 306 | EOS 307 | assert_equal [0, 1], JSON.parse(result) 308 | end 309 | 310 | test "Jsonnet::VM#evaluate returns an error if customized import callback raises an exception" do 311 | vm = Jsonnet::VM.new 312 | called = false 313 | vm.import_callback = ->(base, rel) { called = true; raise } 314 | assert_raise(Jsonnet::EvaluationError) { 315 | vm.evaluate(<<-EOS) 316 | (import "a.jsonnet") + {} 317 | EOS 318 | } 319 | assert_true called 320 | end 321 | 322 | test "Jsonnet::VM#handle_import treats global escapes as define_method does" do 323 | num_eval = 0 324 | begin 325 | bodies = [ 326 | proc {|rel, base| return 'null', '/x.libsonnet' }, 327 | lambda {|rel, base| return 'null', '/x.libsonnet' }, 328 | proc {|rel, base| next 'null', '/x.libsonnet' }, 329 | lambda {|rel, base| next 'null', '/x.libsonnet' }, 330 | proc {|rel, base| break 'null', '/x.libsonnet' }, 331 | lambda {|rel, base| break 'null', '/x.libsonnet' }, 332 | ] 333 | bodies.each do |prc| 334 | vm = Jsonnet::VM.new 335 | vm.handle_import(&prc) 336 | 337 | result = vm.evaluate('import "a.jsonnet"') 338 | assert_nil JSON.load(result) 339 | 340 | num_eval += 1 341 | end 342 | ensure 343 | assert_equal bodies.size, num_eval 344 | end 345 | end 346 | 347 | test "Jsonnet::VM#handle_import is safe on throw" do 348 | [ 349 | proc {|rel, base| throw :dummy }, 350 | lambda {|rel, base| throw :dummy }, 351 | ].each do |prc| 352 | vm = Jsonnet::VM.new 353 | vm.handle_import(&prc) 354 | 355 | catch(:dummy) { 356 | vm.evaluate('import "a.jsonnet"') 357 | flunk "never reach here" 358 | } 359 | end 360 | end 361 | 362 | test "Jsonnet::VM#jpath_add adds a library search path" do 363 | vm = Jsonnet::VM.new 364 | snippet = "(import 'jpath.libsonnet') {b: 2}" 365 | assert_raise(Jsonnet::EvaluationError) { 366 | vm.evaluate(snippet) 367 | } 368 | 369 | vm.jpath_add(File.join(__dir__, 'fixtures')) 370 | result = vm.evaluate(snippet) 371 | assert_equal JSON.parse(<<-EOS), JSON.parse(result) 372 | { 373 | "a": 1, 374 | "b": 2 375 | } 376 | EOS 377 | end 378 | 379 | test "Jsonnet::VM#define_function adds a new native extension" do 380 | vm = Jsonnet::VM.new 381 | called = false 382 | 383 | vm.define_function("myPow") do |x, y| 384 | called = true 385 | x ** y 386 | end 387 | 388 | result = vm.evaluate("std.native('myPow')(3, 4)") 389 | assert_equal 3**4, JSON.load(result) 390 | assert_true called 391 | end 392 | 393 | test "Jsonnet::VM#define_function passes various types of arguments" do 394 | [ 395 | [%q(null), nil], 396 | [%q("abc"), "abc"], 397 | [%q(1), 1.0], 398 | [%q(1.25), 1.25], 399 | [%q(true), true], 400 | [%q(false), false], 401 | ].each do |expr, value| 402 | vm = Jsonnet::VM.new 403 | vm.define_function("myFunc") do |x| 404 | assert_equal value, x 405 | next nil 406 | end 407 | vm.evaluate("std.native('myFunc')(#{expr})") 408 | end 409 | end 410 | 411 | test "Jsonnet::VM#define_function returns various types of values" do 412 | [ 413 | [nil, nil], 414 | ["abc", "abc"], 415 | [1, 1.0], 416 | [1.25, 1.25], 417 | [true, true], 418 | [false, false], 419 | ].each do |retval, expected| 420 | vm = Jsonnet::VM.new 421 | vm.define_function("myFunc") { retval } 422 | 423 | result = vm.evaluate("std.native('myFunc')()") 424 | assert_equal expected, JSON.load(result) 425 | end 426 | end 427 | 428 | test "Jsonnet::VM#define_function translates an exception in a native function into an error" do 429 | vm = Jsonnet::VM.new 430 | vm.define_function("myFunc") do |x| 431 | raise "something wrong" 432 | end 433 | assert_raise(Jsonnet::EvaluationError) { 434 | vm.evaluate("std.native('myFunc')(1)") 435 | } 436 | end 437 | 438 | test "Jsonnet::VM#define_function let the function return a compound object" do 439 | vm = Jsonnet::VM.new 440 | vm.define_function("myCompound") do |x, y| 441 | { 442 | x => y, 443 | y => [x, y, y, x], 444 | } 445 | end 446 | 447 | result = vm.evaluate("std.native('myCompound')('abc', 'def')") 448 | assert_equal JSON.parse(<<-EOS), JSON.parse(result) 449 | { 450 | "abc": "def", 451 | "def": ["abc", "def", "def", "abc"] 452 | } 453 | EOS 454 | end 455 | 456 | test "Jsonnet::VM#define_function treats global escapes as define_method does" do 457 | num_eval = 0 458 | begin 459 | bodies = [ 460 | proc {|x| return x }, 461 | lambda {|x| return x }, 462 | proc {|x| next x }, 463 | lambda {|x| next x }, 464 | proc {|x| break x }, 465 | lambda {|x| break x }, 466 | ] 467 | bodies.each do |prc| 468 | vm = Jsonnet::VM.new 469 | vm.define_function(:myFunc, prc) 470 | 471 | result = vm.evaluate('std.native("myFunc")(1.25) + 0.25') 472 | assert_equal 1.25 + 0.25, JSON.load(result) 473 | 474 | num_eval += 1 475 | end 476 | ensure 477 | assert_equal bodies.size, num_eval 478 | end 479 | end 480 | 481 | test "Jsonnet::VM#define_function is safe on throw" do 482 | [ 483 | proc {|x| throw :dummy }, 484 | lambda {|x| throw :dummy }, 485 | ].each do |prc| 486 | vm = Jsonnet::VM.new 487 | vm.define_function(:myFunc, prc) 488 | 489 | catch(:dummy) { 490 | vm.evaluate('std.native("myFunc")(1.234)') 491 | flunk "never reach here" 492 | } 493 | end 494 | end 495 | 496 | test "Jsonnet::VM#format_file formats Jsonnet file" do 497 | vm = Jsonnet::VM.new 498 | vm.fmt_indent = 4 499 | with_example_file(%< 500 | local myvar = 1; 501 | { 502 | "foo": myvar 503 | } 504 | >) {|fname| 505 | result = vm.format_file(fname) 506 | assert_equal <<-EOS, result 507 | local myvar = 1; 508 | { 509 | foo: myvar, 510 | } 511 | EOS 512 | } 513 | end 514 | 515 | test "Jsonnet::VM#format formats Jsonnet snippet" do 516 | vm = Jsonnet::VM.new 517 | vm.fmt_string = 'd' 518 | result = vm.format(<<-EOS) 519 | local myvar = 'myvar'; 520 | { 521 | foo: [myvar,myvar] 522 | } 523 | EOS 524 | assert_equal <<-EOS, result 525 | local myvar = "myvar"; 526 | { 527 | foo: [myvar, myvar], 528 | } 529 | EOS 530 | end 531 | 532 | test "Jsonnet::VM#fmt_string only accepts 'd', 's', or 'l'" do 533 | vm = Jsonnet::VM.new 534 | vm.fmt_string = Jsonnet::STRING_STYLE_DOUBLE 535 | vm.fmt_string = Jsonnet::STRING_STYLE_SINGLE 536 | vm.fmt_string = Jsonnet::STRING_STYLE_LEAVE 537 | assert_raise(ArgumentError) do 538 | vm.fmt_string = '' 539 | end 540 | assert_raise(ArgumentError) do 541 | vm.fmt_string = 'a' 542 | end 543 | assert_raise(ArgumentError) do 544 | vm.fmt_string = 'ds' 545 | end 546 | assert_raise(TypeError) do 547 | vm.fmt_string = 0 548 | end 549 | end 550 | 551 | test "Jsonnet::VM#fmt_comment only accepts 'h', 's', or 'l'" do 552 | vm = Jsonnet::VM.new 553 | vm.fmt_comment = Jsonnet::COMMENT_STYLE_HASH 554 | vm.fmt_comment = Jsonnet::COMMENT_STYLE_SLASH 555 | vm.fmt_comment = Jsonnet::COMMENT_STYLE_LEAVE 556 | assert_raise(ArgumentError) do 557 | vm.fmt_comment = '' 558 | end 559 | assert_raise(ArgumentError) do 560 | vm.fmt_comment = 'a' 561 | end 562 | assert_raise(ArgumentError) do 563 | vm.fmt_comment = 'hs' 564 | end 565 | assert_raise(TypeError) do 566 | vm.fmt_comment = 0 567 | end 568 | end 569 | 570 | test "Jsonnet::VM#fmt_file raises FormatError on error" do 571 | vm = Jsonnet::VM.new 572 | with_example_file('{foo: }') do |fname| 573 | assert_raise(Jsonnet::FormatError) do 574 | vm.format_file(fname) 575 | end 576 | end 577 | end 578 | 579 | test "Jsonnet::VM#fmt_snippet raises FormatError on error" do 580 | vm = Jsonnet::VM.new 581 | assert_raise(Jsonnet::FormatError) do 582 | vm.format('{foo: }') 583 | end 584 | end 585 | 586 | private 587 | def with_example_file(content) 588 | Tempfile.open("example.jsonnet") {|f| 589 | f.print content 590 | f.flush 591 | f.rewind 592 | yield f.path 593 | } 594 | end 595 | end 596 | 597 | --------------------------------------------------------------------------------