From a81da12bb2a52ba496e8df9caea7e01503cdcfba Mon Sep 17 00:00:00 2001 From: Michael Koziarski Date: Thu, 22 May 2014 15:30:21 +1200 Subject: [PATCH 1/2] Add a :sort_keys option to Encoder This allows people to canonicalize JSON and ensure documents are identical irrespective of the order keys were added to the underlying Hash. --- ext/yajl/yajl_ext.c | 11 ++++++++++- ext/yajl/yajl_ext.h | 5 +++-- lib/yajl.rb | 4 +++- spec/encoding/encoding_spec.rb | 18 +++++++++++++++++- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/ext/yajl/yajl_ext.c b/ext/yajl/yajl_ext.c index c2b1c89d..fb0b7ab6 100644 --- a/ext/yajl/yajl_ext.c +++ b/ext/yajl/yajl_ext.c @@ -165,6 +165,9 @@ void yajl_encode_part(void * wrapper, VALUE obj, VALUE io) { /* TODO: itterate through keys in the hash */ keys = rb_funcall(obj, intern_keys, 0); + if (w->sortKeys) { + keys = rb_funcall(keys, intern_sort, 0); + } for(idx=0; idxindentString = actualIndent; wrapper->encoder = yajl_gen_alloc(&cfg, NULL); wrapper->on_progress_callback = Qnil; + wrapper->sortKeys = sortKeys; if (opts != Qnil && rb_funcall(opts, intern_has_key, 1, sym_terminator) == Qtrue) { wrapper->terminator = rb_hash_aref(opts, sym_terminator); #ifdef HAVE_RUBY_ENCODING_H @@ -922,10 +929,12 @@ void Init_yajl() { intern_to_sym = rb_intern("to_sym"); intern_has_key = rb_intern("has_key?"); intern_as_json = rb_intern("as_json"); + intern_sort = rb_intern("sort"); sym_allow_comments = ID2SYM(rb_intern("allow_comments")); sym_check_utf8 = ID2SYM(rb_intern("check_utf8")); sym_pretty = ID2SYM(rb_intern("pretty")); + sym_sort_keys = ID2SYM(rb_intern("sort_keys")); sym_indent = ID2SYM(rb_intern("indent")); sym_html_safe = ID2SYM(rb_intern("html_safe")); sym_terminator = ID2SYM(rb_intern("terminator")); diff --git a/ext/yajl/yajl_ext.h b/ext/yajl/yajl_ext.h index e0f5948b..ec3dec39 100644 --- a/ext/yajl/yajl_ext.h +++ b/ext/yajl/yajl_ext.h @@ -55,8 +55,8 @@ static rb_encoding *utf8Encoding; static VALUE cParseError, cEncodeError, mYajl, cParser, cEncoder; static ID intern_io_read, intern_call, intern_keys, intern_to_s, - intern_to_json, intern_has_key, intern_to_sym, intern_as_json; -static ID sym_allow_comments, sym_check_utf8, sym_pretty, sym_indent, sym_terminator, sym_symbolize_keys, sym_symbolize_names, sym_html_safe; + intern_to_json, intern_has_key, intern_to_sym, intern_as_json, intern_sort; +static ID sym_allow_comments, sym_check_utf8, sym_pretty, sym_sort_keys, sym_indent, sym_terminator, sym_symbolize_keys, sym_symbolize_names, sym_html_safe; #define GetParser(obj, sval) Data_Get_Struct(obj, yajl_parser_wrapper, sval); #define GetEncoder(obj, sval) Data_Get_Struct(obj, yajl_encoder_wrapper, sval); @@ -105,6 +105,7 @@ typedef struct { VALUE terminator; yajl_gen encoder; unsigned char *indentString; + int sortKeys; } yajl_encoder_wrapper; static VALUE rb_yajl_parser_new(int argc, VALUE * argv, VALUE self); diff --git a/lib/yajl.rb b/lib/yajl.rb index a9bfb163..e5138be1 100644 --- a/lib/yajl.rb +++ b/lib/yajl.rb @@ -57,6 +57,8 @@ class Encoder # # :indent accepts a string and will be used as the indent character(s) during the pretty print process # + # :sort_keys accepts a boolean and will cause the keys of an object to be output in sorted order + # # If a block is passed, it will be used as (and work the same as) the +on_progress+ callback def self.encode(obj, *args, &block) # TODO: this code smells, any ideas? @@ -73,4 +75,4 @@ def self.encode(obj, *args, &block) new(options).encode(obj, io, &block) end end -end \ No newline at end of file +end diff --git a/spec/encoding/encoding_spec.rb b/spec/encoding/encoding_spec.rb index 818f028b..6a4919e8 100644 --- a/spec/encoding/encoding_spec.rb +++ b/spec/encoding/encoding_spec.rb @@ -312,4 +312,20 @@ def to_s Yajl::Encoder.encode(root) }.should raise_error(Yajl::EncodeError) end -end \ No newline at end of file + + it "should sort keys when asked to" do + a = {} + a["z"] = 1 + a["a"] = 2 + + Yajl::Encoder.encode(a, :sort_keys=> true).should eql(%({"a":2,"z":1})) + end + it "should not sort keys when not asked to" do + a = {} + a["z"] = 1 + a["a"] = 2 + + Yajl::Encoder.encode(a).should eql(%({"z":1,"a":2})) + end + +end From 20c1195d6bc93d202007b1a9510e3a06032bdcfc Mon Sep 17 00:00:00 2001 From: Michael Koziarski Date: Fri, 23 May 2014 12:09:46 +1200 Subject: [PATCH 2/2] Test case that fails --- spec/encoding/encoding_spec.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spec/encoding/encoding_spec.rb b/spec/encoding/encoding_spec.rb index 6a4919e8..f34d3bf7 100644 --- a/spec/encoding/encoding_spec.rb +++ b/spec/encoding/encoding_spec.rb @@ -320,6 +320,7 @@ def to_s Yajl::Encoder.encode(a, :sort_keys=> true).should eql(%({"a":2,"z":1})) end + it "should not sort keys when not asked to" do a = {} a["z"] = 1 @@ -328,4 +329,13 @@ def to_s Yajl::Encoder.encode(a).should eql(%({"z":1,"a":2})) end + it "should sort keys without error when the keys are various crazy things" do + a = {} + a[:hello] = 1 + a[0] = 2 + a[Yajl] = 3 + + Yajl::Encoder.encode(a, :sort_keys=>true).should eql(%({"0":2,"Yajl":3,"hello":1})) + end + end