Skip to content
Browse files

Isue #11 - ml env bootstrap doesn't complain on error

added code to catch errors and pretty print them.
  • Loading branch information...
1 parent fd2861e commit 4a9fd206aba595b84b332bedcca33dd4b5276abb @paxtonhare paxtonhare committed
View
0 deploy/lib/Http.rb → deploy/lib/RoxyHttp.rb
File renamed without changes.
View
62 deploy/lib/json.rb
@@ -0,0 +1,62 @@
+##
+# = JavaScript Object Notation (JSON)
+#
+# JSON is a lightweight data-interchange format. It is easy for us
+# humans to read and write. Plus, equally simple for machines to generate or parse.
+# JSON is completely language agnostic, making it the ideal interchange format.
+#
+# Built on two universally available structures:
+# 1. A collection of name/value pairs. Often referred to as an _object_, hash table, record, struct, keyed list, or associative array.
+# 2. An orderd list of values. More commonly named as an _array_, vector, sequence, or list.
+#
+# To read more about JSON visit: http://json.org
+#
+# == Parsing JSON
+#
+# To parse a JSON string received by another application, or generated within
+# your existing application:
+#
+# require 'json'
+#
+# my_hash = JSON.parse('{"hello": "goodbye"}')
+# puts my_hash["hello"] => "goodbye"
+#
+# Notice the extra quotes <tt>''</tt> around the hash notation. Ruby expects
+# the argument to be a string and can't convert objects like a hash or array.
+#
+# Ruby converts your string into a hash
+#
+# == Generating JSON
+#
+# Creating a JSON string for communication or serialization is
+# just as simple.
+#
+# require 'json'
+#
+# my_hash = {:hello => "goodbye"}
+# puts JSON.generate(my_hash) => "{\"hello\":\"goodbye\"}"
+#
+# Or an alternative way:
+#
+# require 'json'
+# puts {:hello => "goodbye"}.to_json => "{\"hello\":\"goodbye\"}"
+#
+# <tt>JSON.generate</tt> only allows objects or arrays to be converted
+# to JSON syntax. While <tt>to_json</tt> accepts many Ruby classes
+# even though it only acts a method for serialization:
+#
+# require 'json'
+#
+# 1.to_json => "1"
+#
+
+require 'json/common'
+module JSON
+ require 'json/version'
+
+ begin
+ require 'json/ext'
+ rescue LoadError
+ require 'json/pure'
+ end
+end
View
21 deploy/lib/json/Array.xpm
@@ -0,0 +1,21 @@
+/* XPM */
+static char * Array_xpm[] = {
+"16 16 2 1",
+" c None",
+". c #000000",
+" ",
+" ",
+" ",
+" .......... ",
+" . . ",
+" . . ",
+" . . ",
+" . . ",
+" . . ",
+" . . ",
+" . . ",
+" . . ",
+" .......... ",
+" ",
+" ",
+" "};
View
21 deploy/lib/json/FalseClass.xpm
@@ -0,0 +1,21 @@
+/* XPM */
+static char * False_xpm[] = {
+"16 16 2 1",
+" c None",
+". c #FF0000",
+" ",
+" ",
+" ",
+" ...... ",
+" . ",
+" . ",
+" . ",
+" ...... ",
+" . ",
+" . ",
+" . ",
+" . ",
+" . ",
+" ",
+" ",
+" "};
View
21 deploy/lib/json/Hash.xpm
@@ -0,0 +1,21 @@
+/* XPM */
+static char * Hash_xpm[] = {
+"16 16 2 1",
+" c None",
+". c #000000",
+" ",
+" ",
+" ",
+" . . ",
+" . . ",
+" . . ",
+" ......... ",
+" . . ",
+" . . ",
+" ......... ",
+" . . ",
+" . . ",
+" . . ",
+" ",
+" ",
+" "};
View
73 deploy/lib/json/Key.xpm
@@ -0,0 +1,73 @@
+/* XPM */
+static char * Key_xpm[] = {
+"16 16 54 1",
+" c None",
+". c #110007",
+"+ c #0E0900",
+"@ c #000013",
+"# c #070600",
+"$ c #F6F006",
+"% c #ECE711",
+"& c #E5EE00",
+"* c #16021E",
+"= c #120900",
+"- c #EDF12B",
+"; c #000033",
+"> c #0F0000",
+", c #FFFE03",
+"' c #E6E500",
+") c #16021B",
+"! c #F7F502",
+"~ c #000E00",
+"{ c #130000",
+"] c #FFF000",
+"^ c #FFE711",
+"/ c #140005",
+"( c #190025",
+"_ c #E9DD27",
+": c #E7DC04",
+"< c #FFEC09",
+"[ c #FFE707",
+"} c #FFDE10",
+"| c #150021",
+"1 c #160700",
+"2 c #FAF60E",
+"3 c #EFE301",
+"4 c #FEF300",
+"5 c #E7E000",
+"6 c #FFFF08",
+"7 c #0E0206",
+"8 c #040000",
+"9 c #03052E",
+"0 c #041212",
+"a c #070300",
+"b c #F2E713",
+"c c #F9DE13",
+"d c #36091E",
+"e c #00001C",
+"f c #1F0010",
+"g c #FFF500",
+"h c #DEDE00",
+"i c #050A00",
+"j c #FAF14A",
+"k c #F5F200",
+"l c #040404",
+"m c #1A0D00",
+"n c #EDE43D",
+"o c #ECE007",
+" ",
+" ",
+" .+@ ",
+" #$%&* ",
+" =-;>,') ",
+" >!~{]^/ ",
+" (_:<[}| ",
+" 1234567 ",
+" 890abcd ",
+" efghi ",
+" >jkl ",
+" mnol ",
+" >kl ",
+" ll ",
+" ",
+" "};
View
21 deploy/lib/json/NilClass.xpm
@@ -0,0 +1,21 @@
+/* XPM */
+static char * False_xpm[] = {
+"16 16 2 1",
+" c None",
+". c #000000",
+" ",
+" ",
+" ",
+" ... ",
+" . . ",
+" . . ",
+" . . ",
+" . . ",
+" . . ",
+" . . ",
+" . . ",
+" . . ",
+" ... ",
+" ",
+" ",
+" "};
View
28 deploy/lib/json/Numeric.xpm
@@ -0,0 +1,28 @@
+/* XPM */
+static char * Numeric_xpm[] = {
+"16 16 9 1",
+" c None",
+". c #FF0000",
+"+ c #0000FF",
+"@ c #0023DB",
+"# c #00EA14",
+"$ c #00FF00",
+"% c #004FAF",
+"& c #0028D6",
+"* c #00F20C",
+" ",
+" ",
+" ",
+" ... +++@#$$$$ ",
+" .+ %& $$ ",
+" . + $ ",
+" . + $$ ",
+" . ++$$$$ ",
+" . + $$ ",
+" . + $ ",
+" . + $ ",
+" . + $ $$ ",
+" .....++++*$$ ",
+" ",
+" ",
+" "};
View
96 deploy/lib/json/String.xpm
@@ -0,0 +1,96 @@
+/* XPM */
+static char * String_xpm[] = {
+"16 16 77 1",
+" c None",
+". c #000000",
+"+ c #040404",
+"@ c #080806",
+"# c #090606",
+"$ c #EEEAE1",
+"% c #E7E3DA",
+"& c #E0DBD1",
+"* c #D4B46F",
+"= c #0C0906",
+"- c #E3C072",
+"; c #E4C072",
+"> c #060505",
+", c #0B0A08",
+"' c #D5B264",
+") c #D3AF5A",
+"! c #080602",
+"~ c #E1B863",
+"{ c #DDB151",
+"] c #DBAE4A",
+"^ c #DDB152",
+"/ c #DDB252",
+"( c #070705",
+"_ c #0C0A07",
+": c #D3A33B",
+"< c #020201",
+"[ c #DAAA41",
+"} c #040302",
+"| c #E4D9BF",
+"1 c #0B0907",
+"2 c #030201",
+"3 c #020200",
+"4 c #C99115",
+"5 c #080704",
+"6 c #DBC8A2",
+"7 c #E7D7B4",
+"8 c #E0CD9E",
+"9 c #080601",
+"0 c #040400",
+"a c #010100",
+"b c #0B0B08",
+"c c #DCBF83",
+"d c #DCBC75",
+"e c #DEB559",
+"f c #040301",
+"g c #BC8815",
+"h c #120E07",
+"i c #060402",
+"j c #0A0804",
+"k c #D4A747",
+"l c #D6A12F",
+"m c #0E0C05",
+"n c #C8C1B0",
+"o c #1D1B15",
+"p c #D7AD51",
+"q c #070502",
+"r c #080804",
+"s c #BC953B",
+"t c #C4BDAD",
+"u c #0B0807",
+"v c #DBAC47",
+"w c #1B150A",
+"x c #B78A2C",
+"y c #D8A83C",
+"z c #D4A338",
+"A c #0F0B03",
+"B c #181105",
+"C c #C59325",
+"D c #C18E1F",
+"E c #060600",
+"F c #CC992D",
+"G c #B98B25",
+"H c #B3831F",
+"I c #C08C1C",
+"J c #060500",
+"K c #0E0C03",
+"L c #0D0A00",
+" ",
+" .+@# ",
+" .$%&*= ",
+" .-;>,')! ",
+" .~. .{]. ",
+" .^/. (_:< ",
+" .[.}|$12 ",
+" 345678}90 ",
+" a2bcdefgh ",
+" ijkl.mno ",
+" <pq. rstu ",
+" .]v. wx= ",
+" .yzABCDE ",
+" .FGHIJ ",
+" 0KL0 ",
+" "};
View
21 deploy/lib/json/TrueClass.xpm
@@ -0,0 +1,21 @@
+/* XPM */
+static char * TrueClass_xpm[] = {
+"16 16 2 1",
+" c None",
+". c #0BF311",
+" ",
+" ",
+" ",
+" ......... ",
+" . ",
+" . ",
+" . ",
+" . ",
+" . ",
+" . ",
+" . ",
+" . ",
+" . ",
+" ",
+" ",
+" "};
View
243 deploy/lib/json/add/core.rb
@@ -0,0 +1,243 @@
+# This file contains implementations of ruby core's custom objects for
+# serialisation/deserialisation.
+
+unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
+ require 'json'
+end
+require 'date'
+
+# Symbol serialization/deserialization
+class Symbol
+ # Returns a hash, that will be turned into a JSON object and represent this
+ # object.
+ def as_json(*)
+ {
+ JSON.create_id => self.class.name,
+ 's' => to_s,
+ }
+ end
+
+ # Stores class name (Symbol) with String representation of Symbol as a JSON string.
+ def to_json(*a)
+ as_json.to_json(*a)
+ end
+
+ # Deserializes JSON string by converting the <tt>string</tt> value stored in the object to a Symbol
+ def self.json_create(o)
+ o['s'].to_sym
+ end
+end
+
+# Time serialization/deserialization
+class Time
+
+ # Deserializes JSON string by converting time since epoch to Time
+ def self.json_create(object)
+ if usec = object.delete('u') # used to be tv_usec -> tv_nsec
+ object['n'] = usec * 1000
+ end
+ if respond_to?(:tv_nsec)
+ at(*object.values_at('s', 'n'))
+ else
+ at(object['s'], object['n'] / 1000)
+ end
+ end
+
+ # Returns a hash, that will be turned into a JSON object and represent this
+ # object.
+ def as_json(*)
+ {
+ JSON.create_id => self.class.name,
+ 's' => tv_sec,
+ 'n' => respond_to?(:tv_nsec) ? tv_nsec : tv_usec * 1000
+ }
+ end
+
+ # Stores class name (Time) with number of seconds since epoch and number of
+ # microseconds for Time as JSON string
+ def to_json(*args)
+ as_json.to_json(*args)
+ end
+end
+
+# Date serialization/deserialization
+class Date
+
+ # Deserializes JSON string by converting Julian year <tt>y</tt>, month
+ # <tt>m</tt>, day <tt>d</tt> and Day of Calendar Reform <tt>sg</tt> to Date.
+ def self.json_create(object)
+ civil(*object.values_at('y', 'm', 'd', 'sg'))
+ end
+
+ alias start sg unless method_defined?(:start)
+
+ # Returns a hash, that will be turned into a JSON object and represent this
+ # object.
+ def as_json(*)
+ {
+ JSON.create_id => self.class.name,
+ 'y' => year,
+ 'm' => month,
+ 'd' => day,
+ 'sg' => start,
+ }
+ end
+
+ # Stores class name (Date) with Julian year <tt>y</tt>, month <tt>m</tt>, day
+ # <tt>d</tt> and Day of Calendar Reform <tt>sg</tt> as JSON string
+ def to_json(*args)
+ as_json.to_json(*args)
+ end
+end
+
+# DateTime serialization/deserialization
+class DateTime
+
+ # Deserializes JSON string by converting year <tt>y</tt>, month <tt>m</tt>,
+ # day <tt>d</tt>, hour <tt>H</tt>, minute <tt>M</tt>, second <tt>S</tt>,
+ # offset <tt>of</tt> and Day of Calendar Reform <tt>sg</tt> to DateTime.
+ def self.json_create(object)
+ args = object.values_at('y', 'm', 'd', 'H', 'M', 'S')
+ of_a, of_b = object['of'].split('/')
+ if of_b and of_b != '0'
+ args << Rational(of_a.to_i, of_b.to_i)
+ else
+ args << of_a
+ end
+ args << object['sg']
+ civil(*args)
+ end
+
+ alias start sg unless method_defined?(:start)
+
+ # Returns a hash, that will be turned into a JSON object and represent this
+ # object.
+ def as_json(*)
+ {
+ JSON.create_id => self.class.name,
+ 'y' => year,
+ 'm' => month,
+ 'd' => day,
+ 'H' => hour,
+ 'M' => min,
+ 'S' => sec,
+ 'of' => offset.to_s,
+ 'sg' => start,
+ }
+ end
+
+ # Stores class name (DateTime) with Julian year <tt>y</tt>, month <tt>m</tt>,
+ # day <tt>d</tt>, hour <tt>H</tt>, minute <tt>M</tt>, second <tt>S</tt>,
+ # offset <tt>of</tt> and Day of Calendar Reform <tt>sg</tt> as JSON string
+ def to_json(*args)
+ as_json.to_json(*args)
+ end
+end
+
+# Range serialization/deserialization
+class Range
+
+ # Deserializes JSON string by constructing new Range object with arguments
+ # <tt>a</tt> serialized by <tt>to_json</tt>.
+ def self.json_create(object)
+ new(*object['a'])
+ end
+
+ # Returns a hash, that will be turned into a JSON object and represent this
+ # object.
+ def as_json(*)
+ {
+ JSON.create_id => self.class.name,
+ 'a' => [ first, last, exclude_end? ]
+ }
+ end
+
+ # Stores class name (Range) with JSON array of arguments <tt>a</tt> which
+ # include <tt>first</tt> (integer), <tt>last</tt> (integer), and
+ # <tt>exclude_end?</tt> (boolean) as JSON string.
+ def to_json(*args)
+ as_json.to_json(*args)
+ end
+end
+
+# Struct serialization/deserialization
+class Struct
+
+ # Deserializes JSON string by constructing new Struct object with values
+ # <tt>v</tt> serialized by <tt>to_json</tt>.
+ def self.json_create(object)
+ new(*object['v'])
+ end
+
+ # Returns a hash, that will be turned into a JSON object and represent this
+ # object.
+ def as_json(*)
+ klass = self.class.name
+ klass.to_s.empty? and raise JSON::JSONError, "Only named structs are supported!"
+ {
+ JSON.create_id => klass,
+ 'v' => values,
+ }
+ end
+
+ # Stores class name (Struct) with Struct values <tt>v</tt> as a JSON string.
+ # Only named structs are supported.
+ def to_json(*args)
+ as_json.to_json(*args)
+ end
+end
+
+# Exception serialization/deserialization
+class Exception
+
+ # Deserializes JSON string by constructing new Exception object with message
+ # <tt>m</tt> and backtrace <tt>b</tt> serialized with <tt>to_json</tt>
+ def self.json_create(object)
+ result = new(object['m'])
+ result.set_backtrace object['b']
+ result
+ end
+
+ # Returns a hash, that will be turned into a JSON object and represent this
+ # object.
+ def as_json(*)
+ {
+ JSON.create_id => self.class.name,
+ 'm' => message,
+ 'b' => backtrace,
+ }
+ end
+
+ # Stores class name (Exception) with message <tt>m</tt> and backtrace array
+ # <tt>b</tt> as JSON string
+ def to_json(*args)
+ as_json.to_json(*args)
+ end
+end
+
+# Regexp serialization/deserialization
+class Regexp
+
+ # Deserializes JSON string by constructing new Regexp object with source
+ # <tt>s</tt> (Regexp or String) and options <tt>o</tt> serialized by
+ # <tt>to_json</tt>
+ def self.json_create(object)
+ new(object['s'], object['o'])
+ end
+
+ # Returns a hash, that will be turned into a JSON object and represent this
+ # object.
+ def as_json(*)
+ {
+ JSON.create_id => self.class.name,
+ 'o' => options,
+ 's' => source,
+ }
+ end
+
+ # Stores class name (Regexp) with options <tt>o</tt> and source <tt>s</tt>
+ # (Regexp or String) as JSON string
+ def to_json(*)
+ as_json.to_json
+ end
+end
View
8 deploy/lib/json/add/rails.rb
@@ -0,0 +1,8 @@
+# This file used to implementations of rails custom objects for
+# serialisation/deserialisation and is obsoleted now.
+
+unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
+ require 'json'
+end
+
+$DEBUG and warn "required json/add/rails which is obsolete now!"
View
423 deploy/lib/json/common.rb
@@ -0,0 +1,423 @@
+require 'json/version'
+
+module JSON
+ class << self
+ # If _object_ is string-like parse the string and return the parsed result
+ # as a Ruby data structure. Otherwise generate a JSON text from the Ruby
+ # data structure object and return it.
+ #
+ # The _opts_ argument is passed through to generate/parse respectively, see
+ # generate and parse for their documentation.
+ def [](object, opts = {})
+ if object.respond_to? :to_str
+ JSON.parse(object.to_str, opts)
+ else
+ JSON.generate(object, opts)
+ end
+ end
+
+ # Returns the JSON parser class, that is used by JSON. This might be either
+ # JSON::Ext::Parser or JSON::Pure::Parser.
+ attr_reader :parser
+
+ # Set the JSON parser class _parser_ to be used by JSON.
+ def parser=(parser) # :nodoc:
+ @parser = parser
+ remove_const :Parser if JSON.const_defined_in?(self, :Parser)
+ const_set :Parser, parser
+ end
+
+ # Return the constant located at _path_. The format of _path_ has to be
+ # either ::A::B::C or A::B::C. In any case A has to be located at the top
+ # level (absolute namespace path?). If there doesn't exist a constant at
+ # the given path, an ArgumentError is raised.
+ def deep_const_get(path) # :nodoc:
+ path.to_s.split(/::/).inject(Object) do |p, c|
+ case
+ when c.empty? then p
+ when JSON.const_defined_in?(p, c) then p.const_get(c)
+ else
+ begin
+ p.const_missing(c)
+ rescue NameError => e
+ raise ArgumentError, "can't get const #{path}: #{e}"
+ end
+ end
+ end
+ end
+
+ # Set the module _generator_ to be used by JSON.
+ def generator=(generator) # :nodoc:
+ old, $VERBOSE = $VERBOSE, nil
+ @generator = generator
+ generator_methods = generator::GeneratorMethods
+ for const in generator_methods.constants
+ klass = deep_const_get(const)
+ modul = generator_methods.const_get(const)
+ klass.class_eval do
+ instance_methods(false).each do |m|
+ m.to_s == 'to_json' and remove_method m
+ end
+ include modul
+ end
+ end
+ self.state = generator::State
+ const_set :State, self.state
+ const_set :SAFE_STATE_PROTOTYPE, State.new
+ const_set :FAST_STATE_PROTOTYPE, State.new(
+ :indent => '',
+ :space => '',
+ :object_nl => "",
+ :array_nl => "",
+ :max_nesting => false
+ )
+ const_set :PRETTY_STATE_PROTOTYPE, State.new(
+ :indent => ' ',
+ :space => ' ',
+ :object_nl => "\n",
+ :array_nl => "\n"
+ )
+ ensure
+ $VERBOSE = old
+ end
+
+ # Returns the JSON generator modul, that is used by JSON. This might be
+ # either JSON::Ext::Generator or JSON::Pure::Generator.
+ attr_reader :generator
+
+ # Returns the JSON generator state class, that is used by JSON. This might
+ # be either JSON::Ext::Generator::State or JSON::Pure::Generator::State.
+ attr_accessor :state
+
+ # This is create identifier, that is used to decide, if the _json_create_
+ # hook of a class should be called. It defaults to 'json_class'.
+ attr_accessor :create_id
+ end
+ self.create_id = 'json_class'
+
+ NaN = 0.0/0
+
+ Infinity = 1.0/0
+
+ MinusInfinity = -Infinity
+
+ # The base exception for JSON errors.
+ class JSONError < StandardError; end
+
+ # This exception is raised, if a parser error occurs.
+ class ParserError < JSONError; end
+
+ # This exception is raised, if the nesting of parsed datastructures is too
+ # deep.
+ class NestingError < ParserError; end
+
+ # :stopdoc:
+ class CircularDatastructure < NestingError; end
+ # :startdoc:
+
+ # This exception is raised, if a generator or unparser error occurs.
+ class GeneratorError < JSONError; end
+ # For backwards compatibility
+ UnparserError = GeneratorError
+
+ # This exception is raised, if the required unicode support is missing on the
+ # system. Usually this means, that the iconv library is not installed.
+ class MissingUnicodeSupport < JSONError; end
+
+ module_function
+
+ # Parse the JSON document _source_ into a Ruby data structure and return it.
+ #
+ # _opts_ can have the following
+ # keys:
+ # * *max_nesting*: The maximum depth of nesting allowed in the parsed data
+ # structures. Disable depth checking with :max_nesting => false, it defaults
+ # to 19.
+ # * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in
+ # defiance of RFC 4627 to be parsed by the Parser. This option defaults
+ # to false.
+ # * *symbolize_names*: If set to true, returns symbols for the names
+ # (keys) in a JSON object. Otherwise strings are returned, which is also
+ # the default.
+ # * *create_additions*: If set to false, the Parser doesn't create
+ # additions even if a matchin class and create_id was found. This option
+ # defaults to true.
+ # * *object_class*: Defaults to Hash
+ # * *array_class*: Defaults to Array
+ def parse(source, opts = {})
+ Parser.new(source, opts).parse
+ end
+
+ # Parse the JSON document _source_ into a Ruby data structure and return it.
+ # The bang version of the parse method, defaults to the more dangerous values
+ # for the _opts_ hash, so be sure only to parse trusted _source_ documents.
+ #
+ # _opts_ can have the following keys:
+ # * *max_nesting*: The maximum depth of nesting allowed in the parsed data
+ # structures. Enable depth checking with :max_nesting => anInteger. The parse!
+ # methods defaults to not doing max depth checking: This can be dangerous,
+ # if someone wants to fill up your stack.
+ # * *allow_nan*: If set to true, allow NaN, Infinity, and -Infinity in
+ # defiance of RFC 4627 to be parsed by the Parser. This option defaults
+ # to true.
+ # * *create_additions*: If set to false, the Parser doesn't create
+ # additions even if a matchin class and create_id was found. This option
+ # defaults to true.
+ def parse!(source, opts = {})
+ opts = {
+ :max_nesting => false,
+ :allow_nan => true
+ }.update(opts)
+ Parser.new(source, opts).parse
+ end
+
+ # Generate a JSON document from the Ruby data structure _obj_ and return
+ # it. _state_ is * a JSON::State object,
+ # * or a Hash like object (responding to to_hash),
+ # * an object convertible into a hash by a to_h method,
+ # that is used as or to configure a State object.
+ #
+ # It defaults to a state object, that creates the shortest possible JSON text
+ # in one line, checks for circular data structures and doesn't allow NaN,
+ # Infinity, and -Infinity.
+ #
+ # A _state_ hash can have the following keys:
+ # * *indent*: a string used to indent levels (default: ''),
+ # * *space*: a string that is put after, a : or , delimiter (default: ''),
+ # * *space_before*: a string that is put before a : pair delimiter (default: ''),
+ # * *object_nl*: a string that is put at the end of a JSON object (default: ''),
+ # * *array_nl*: a string that is put at the end of a JSON array (default: ''),
+ # * *allow_nan*: true if NaN, Infinity, and -Infinity should be
+ # generated, otherwise an exception is thrown, if these values are
+ # encountered. This options defaults to false.
+ # * *max_nesting*: The maximum depth of nesting allowed in the data
+ # structures from which JSON is to be generated. Disable depth checking
+ # with :max_nesting => false, it defaults to 19.
+ #
+ # See also the fast_generate for the fastest creation method with the least
+ # amount of sanity checks, and the pretty_generate method for some
+ # defaults for a pretty output.
+ def generate(obj, opts = nil)
+ state = SAFE_STATE_PROTOTYPE.dup
+ if opts
+ if opts.respond_to? :to_hash
+ opts = opts.to_hash
+ elsif opts.respond_to? :to_h
+ opts = opts.to_h
+ else
+ raise TypeError, "can't convert #{opts.class} into Hash"
+ end
+ state = state.configure(opts)
+ end
+ state.generate(obj)
+ end
+
+ # :stopdoc:
+ # I want to deprecate these later, so I'll first be silent about them, and
+ # later delete them.
+ alias unparse generate
+ module_function :unparse
+ # :startdoc:
+
+ # Generate a JSON document from the Ruby data structure _obj_ and return it.
+ # This method disables the checks for circles in Ruby objects.
+ #
+ # *WARNING*: Be careful not to pass any Ruby data structures with circles as
+ # _obj_ argument, because this will cause JSON to go into an infinite loop.
+ def fast_generate(obj, opts = nil)
+ state = FAST_STATE_PROTOTYPE.dup
+ if opts
+ if opts.respond_to? :to_hash
+ opts = opts.to_hash
+ elsif opts.respond_to? :to_h
+ opts = opts.to_h
+ else
+ raise TypeError, "can't convert #{opts.class} into Hash"
+ end
+ state.configure(opts)
+ end
+ state.generate(obj)
+ end
+
+ # :stopdoc:
+ # I want to deprecate these later, so I'll first be silent about them, and later delete them.
+ alias fast_unparse fast_generate
+ module_function :fast_unparse
+ # :startdoc:
+
+ # Generate a JSON document from the Ruby data structure _obj_ and return it.
+ # The returned document is a prettier form of the document returned by
+ # #unparse.
+ #
+ # The _opts_ argument can be used to configure the generator, see the
+ # generate method for a more detailed explanation.
+ def pretty_generate(obj, opts = nil)
+ state = PRETTY_STATE_PROTOTYPE.dup
+ if opts
+ if opts.respond_to? :to_hash
+ opts = opts.to_hash
+ elsif opts.respond_to? :to_h
+ opts = opts.to_h
+ else
+ raise TypeError, "can't convert #{opts.class} into Hash"
+ end
+ state.configure(opts)
+ end
+ state.generate(obj)
+ end
+
+ # :stopdoc:
+ # I want to deprecate these later, so I'll first be silent about them, and later delete them.
+ alias pretty_unparse pretty_generate
+ module_function :pretty_unparse
+ # :startdoc:
+
+ # Load a ruby data structure from a JSON _source_ and return it. A source can
+ # either be a string-like object, an IO like object, or an object responding
+ # to the read method. If _proc_ was given, it will be called with any nested
+ # Ruby object as an argument recursively in depth first order.
+ #
+ # This method is part of the implementation of the load/dump interface of
+ # Marshal and YAML.
+ def load(source, proc = nil)
+ if source.respond_to? :to_str
+ source = source.to_str
+ elsif source.respond_to? :to_io
+ source = source.to_io.read
+ else
+ source = source.read
+ end
+ result = parse(source, :max_nesting => false, :allow_nan => true)
+ recurse_proc(result, &proc) if proc
+ result
+ end
+
+ # Recursively calls passed _Proc_ if the parsed data structure is an _Array_ or _Hash_
+ def recurse_proc(result, &proc)
+ case result
+ when Array
+ result.each { |x| recurse_proc x, &proc }
+ proc.call result
+ when Hash
+ result.each { |x, y| recurse_proc x, &proc; recurse_proc y, &proc }
+ proc.call result
+ else
+ proc.call result
+ end
+ end
+
+ alias restore load
+ module_function :restore
+
+ # Dumps _obj_ as a JSON string, i.e. calls generate on the object and returns
+ # the result.
+ #
+ # If anIO (an IO like object or an object that responds to the write method)
+ # was given, the resulting JSON is written to it.
+ #
+ # If the number of nested arrays or objects exceeds _limit_ an ArgumentError
+ # exception is raised. This argument is similar (but not exactly the
+ # same!) to the _limit_ argument in Marshal.dump.
+ #
+ # This method is part of the implementation of the load/dump interface of
+ # Marshal and YAML.
+ def dump(obj, anIO = nil, limit = nil)
+ if anIO and limit.nil?
+ anIO = anIO.to_io if anIO.respond_to?(:to_io)
+ unless anIO.respond_to?(:write)
+ limit = anIO
+ anIO = nil
+ end
+ end
+ limit ||= 0
+ result = generate(obj, :allow_nan => true, :max_nesting => limit)
+ if anIO
+ anIO.write result
+ anIO
+ else
+ result
+ end
+ rescue JSON::NestingError
+ raise ArgumentError, "exceed depth limit"
+ end
+
+ # Swap consecutive bytes of _string_ in place.
+ def self.swap!(string) # :nodoc:
+ 0.upto(string.size / 2) do |i|
+ break unless string[2 * i + 1]
+ string[2 * i], string[2 * i + 1] = string[2 * i + 1], string[2 * i]
+ end
+ string
+ end
+
+ # Shortuct for iconv.
+ if ::String.method_defined?(:encode)
+ # Encodes string using Ruby's _String.encode_
+ def self.iconv(to, from, string)
+ string.encode(to, from)
+ end
+ else
+ require 'iconv'
+ # Encodes string using _iconv_ library
+ def self.iconv(to, from, string)
+ Iconv.iconv(to, from, string).first
+ end
+ end
+
+ if ::Object.method(:const_defined?).arity == 1
+ def self.const_defined_in?(modul, constant)
+ modul.const_defined?(constant)
+ end
+ else
+ def self.const_defined_in?(modul, constant)
+ modul.const_defined?(constant, false)
+ end
+ end
+end
+
+module ::Kernel
+ private
+
+ # Outputs _objs_ to STDOUT as JSON strings in the shortest form, that is in
+ # one line.
+ def j(*objs)
+ objs.each do |obj|
+ puts JSON::generate(obj, :allow_nan => true, :max_nesting => false)
+ end
+ nil
+ end
+
+ # Ouputs _objs_ to STDOUT as JSON strings in a pretty format, with
+ # indentation and over many lines.
+ def jj(*objs)
+ objs.each do |obj|
+ puts JSON::pretty_generate(obj, :allow_nan => true, :max_nesting => false)
+ end
+ nil
+ end
+
+ # If _object_ is string-like parse the string and return the parsed result as
+ # a Ruby data structure. Otherwise generate a JSON text from the Ruby data
+ # structure object and return it.
+ #
+ # The _opts_ argument is passed through to generate/parse respectively, see
+ # generate and parse for their documentation.
+ def JSON(object, *args)
+ if object.respond_to? :to_str
+ JSON.parse(object.to_str, args.first)
+ else
+ JSON.generate(object, args.first)
+ end
+ end
+end
+
+# Extends any Class to include _json_creatable?_ method.
+class ::Class
+ # Returns true, if this class can be used to create an instance
+ # from a serialised JSON string. The class has to implement a class
+ # method _json_create_ that expects a hash as first parameter, which includes
+ # the required data.
+ def json_creatable?
+ respond_to?(:json_create)
+ end
+end
View
1,369 deploy/lib/json/editor.rb
@@ -0,0 +1,1369 @@
+# To use the GUI JSON editor, start the edit_json.rb executable script. It
+# requires ruby-gtk to be installed.
+
+require 'gtk2'
+require 'json'
+require 'rbconfig'
+require 'open-uri'
+
+module JSON
+ module Editor
+ include Gtk
+
+ # Beginning of the editor window title
+ TITLE = 'JSON Editor'.freeze
+
+ # Columns constants
+ ICON_COL, TYPE_COL, CONTENT_COL = 0, 1, 2
+
+ # JSON primitive types (Containers)
+ CONTAINER_TYPES = %w[Array Hash].sort
+ # All JSON primitive types
+ ALL_TYPES = (%w[TrueClass FalseClass Numeric String NilClass] +
+ CONTAINER_TYPES).sort
+
+ # The Nodes necessary for the tree representation of a JSON document
+ ALL_NODES = (ALL_TYPES + %w[Key]).sort
+
+ DEFAULT_DIALOG_KEY_PRESS_HANDLER = lambda do |dialog, event|
+ case event.keyval
+ when Gdk::Keyval::GDK_Return
+ dialog.response Dialog::RESPONSE_ACCEPT
+ when Gdk::Keyval::GDK_Escape
+ dialog.response Dialog::RESPONSE_REJECT
+ end
+ end
+
+ # Returns the Gdk::Pixbuf of the icon named _name_ from the icon cache.
+ def Editor.fetch_icon(name)
+ @icon_cache ||= {}
+ unless @icon_cache.key?(name)
+ path = File.dirname(__FILE__)
+ @icon_cache[name] = Gdk::Pixbuf.new(File.join(path, name + '.xpm'))
+ end
+ @icon_cache[name]
+ end
+
+ # Opens an error dialog on top of _window_ showing the error message
+ # _text_.
+ def Editor.error_dialog(window, text)
+ dialog = MessageDialog.new(window, Dialog::MODAL,
+ MessageDialog::ERROR,
+ MessageDialog::BUTTONS_CLOSE, text)
+ dialog.show_all
+ dialog.run
+ rescue TypeError
+ dialog = MessageDialog.new(Editor.window, Dialog::MODAL,
+ MessageDialog::ERROR,
+ MessageDialog::BUTTONS_CLOSE, text)
+ dialog.show_all
+ dialog.run
+ ensure
+ dialog.destroy if dialog
+ end
+
+ # Opens a yes/no question dialog on top of _window_ showing the error
+ # message _text_. If yes was answered _true_ is returned, otherwise
+ # _false_.
+ def Editor.question_dialog(window, text)
+ dialog = MessageDialog.new(window, Dialog::MODAL,
+ MessageDialog::QUESTION,
+ MessageDialog::BUTTONS_YES_NO, text)
+ dialog.show_all
+ dialog.run do |response|
+ return Gtk::Dialog::RESPONSE_YES === response
+ end
+ ensure
+ dialog.destroy if dialog
+ end
+
+ # Convert the tree model starting from Gtk::TreeIter _iter_ into a Ruby
+ # data structure and return it.
+ def Editor.model2data(iter)
+ return nil if iter.nil?
+ case iter.type
+ when 'Hash'
+ hash = {}
+ iter.each { |c| hash[c.content] = Editor.model2data(c.first_child) }
+ hash
+ when 'Array'
+ array = Array.new(iter.n_children)
+ iter.each_with_index { |c, i| array[i] = Editor.model2data(c) }
+ array
+ when 'Key'
+ iter.content
+ when 'String'
+ iter.content
+ when 'Numeric'
+ content = iter.content
+ if /\./.match(content)
+ content.to_f
+ else
+ content.to_i
+ end
+ when 'TrueClass'
+ true
+ when 'FalseClass'
+ false
+ when 'NilClass'
+ nil
+ else
+ fail "Unknown type found in model: #{iter.type}"
+ end
+ end
+
+ # Convert the Ruby data structure _data_ into tree model data for Gtk and
+ # returns the whole model. If the parameter _model_ wasn't given a new
+ # Gtk::TreeStore is created as the model. The _parent_ parameter specifies
+ # the parent node (iter, Gtk:TreeIter instance) to which the data is
+ # appended, alternativeley the result of the yielded block is used as iter.
+ def Editor.data2model(data, model = nil, parent = nil)
+ model ||= TreeStore.new(Gdk::Pixbuf, String, String)
+ iter = if block_given?
+ yield model
+ else
+ model.append(parent)
+ end
+ case data
+ when Hash
+ iter.type = 'Hash'
+ data.sort.each do |key, value|
+ pair_iter = model.append(iter)
+ pair_iter.type = 'Key'
+ pair_iter.content = key.to_s
+ Editor.data2model(value, model, pair_iter)
+ end
+ when Array
+ iter.type = 'Array'
+ data.each do |value|
+ Editor.data2model(value, model, iter)
+ end
+ when Numeric
+ iter.type = 'Numeric'
+ iter.content = data.to_s
+ when String, true, false, nil
+ iter.type = data.class.name
+ iter.content = data.nil? ? 'null' : data.to_s
+ else
+ iter.type = 'String'
+ iter.content = data.to_s
+ end
+ model
+ end
+
+ # The Gtk::TreeIter class is reopened and some auxiliary methods are added.
+ class Gtk::TreeIter
+ include Enumerable
+
+ # Traverse each of this Gtk::TreeIter instance's children
+ # and yield to them.
+ def each
+ n_children.times { |i| yield nth_child(i) }
+ end
+
+ # Recursively traverse all nodes of this Gtk::TreeIter's subtree
+ # (including self) and yield to them.
+ def recursive_each(&block)
+ yield self
+ each do |i|
+ i.recursive_each(&block)
+ end
+ end
+
+ # Remove the subtree of this Gtk::TreeIter instance from the
+ # model _model_.
+ def remove_subtree(model)
+ while current = first_child
+ model.remove(current)
+ end
+ end
+
+ # Returns the type of this node.
+ def type
+ self[TYPE_COL]
+ end
+
+ # Sets the type of this node to _value_. This implies setting
+ # the respective icon accordingly.
+ def type=(value)
+ self[TYPE_COL] = value
+ self[ICON_COL] = Editor.fetch_icon(value)
+ end
+
+ # Returns the content of this node.
+ def content
+ self[CONTENT_COL]
+ end
+
+ # Sets the content of this node to _value_.
+ def content=(value)
+ self[CONTENT_COL] = value
+ end
+ end
+
+ # This module bundles some method, that can be used to create a menu. It
+ # should be included into the class in question.
+ module MenuExtension
+ include Gtk
+
+ # Creates a Menu, that includes MenuExtension. _treeview_ is the
+ # Gtk::TreeView, on which it operates.
+ def initialize(treeview)
+ @treeview = treeview
+ @menu = Menu.new
+ end
+
+ # Returns the Gtk::TreeView of this menu.
+ attr_reader :treeview
+
+ # Returns the menu.
+ attr_reader :menu
+
+ # Adds a Gtk::SeparatorMenuItem to this instance's #menu.
+ def add_separator
+ menu.append SeparatorMenuItem.new
+ end
+
+ # Adds a Gtk::MenuItem to this instance's #menu. _label_ is the label
+ # string, _klass_ is the item type, and _callback_ is the procedure, that
+ # is called if the _item_ is activated.
+ def add_item(label, keyval = nil, klass = MenuItem, &callback)
+ label = "#{label} (C-#{keyval.chr})" if keyval
+ item = klass.new(label)
+ item.signal_connect(:activate, &callback)
+ if keyval
+ self.signal_connect(:'key-press-event') do |item, event|
+ if event.state & Gdk::Window::ModifierType::CONTROL_MASK != 0 and
+ event.keyval == keyval
+ callback.call item
+ end
+ end
+ end
+ menu.append item
+ item
+ end
+
+ # This method should be implemented in subclasses to create the #menu of
+ # this instance. It has to be called after an instance of this class is
+ # created, to build the menu.
+ def create
+ raise NotImplementedError
+ end
+
+ def method_missing(*a, &b)
+ treeview.__send__(*a, &b)
+ end
+ end
+
+ # This class creates the popup menu, that opens when clicking onto the
+ # treeview.
+ class PopUpMenu
+ include MenuExtension
+
+ # Change the type or content of the selected node.
+ def change_node(item)
+ if current = selection.selected
+ parent = current.parent
+ old_type, old_content = current.type, current.content
+ if ALL_TYPES.include?(old_type)
+ @clipboard_data = Editor.model2data(current)
+ type, content = ask_for_element(parent, current.type,
+ current.content)
+ if type
+ current.type, current.content = type, content
+ current.remove_subtree(model)
+ toplevel.display_status("Changed a node in tree.")
+ window.change
+ end
+ else
+ toplevel.display_status(
+ "Cannot change node of type #{old_type} in tree!")
+ end
+ end
+ end
+
+ # Cut the selected node and its subtree, and save it into the
+ # clipboard.
+ def cut_node(item)
+ if current = selection.selected
+ if current and current.type == 'Key'
+ @clipboard_data = {
+ current.content => Editor.model2data(current.first_child)
+ }
+ else
+ @clipboard_data = Editor.model2data(current)
+ end
+ model.remove(current)
+ window.change
+ toplevel.display_status("Cut a node from tree.")
+ end
+ end
+
+ # Copy the selected node and its subtree, and save it into the
+ # clipboard.
+ def copy_node(item)
+ if current = selection.selected
+ if current and current.type == 'Key'
+ @clipboard_data = {
+ current.content => Editor.model2data(current.first_child)
+ }
+ else
+ @clipboard_data = Editor.model2data(current)
+ end
+ window.change
+ toplevel.display_status("Copied a node from tree.")
+ end
+ end
+
+ # Paste the data in the clipboard into the selected Array or Hash by
+ # appending it.
+ def paste_node_appending(item)
+ if current = selection.selected
+ if @clipboard_data
+ case current.type
+ when 'Array'
+ Editor.data2model(@clipboard_data, model, current)
+ expand_collapse(current)
+ when 'Hash'
+ if @clipboard_data.is_a? Hash
+ parent = current.parent
+ hash = Editor.model2data(current)
+ model.remove(current)
+ hash.update(@clipboard_data)
+ Editor.data2model(hash, model, parent)
+ if parent
+ expand_collapse(parent)
+ elsif @expanded
+ expand_all
+ end
+ window.change
+ else
+ toplevel.display_status(
+ "Cannot paste non-#{current.type} data into '#{current.type}'!")
+ end
+ else
+ toplevel.display_status(
+ "Cannot paste node below '#{current.type}'!")
+ end
+ else
+ toplevel.display_status("Nothing to paste in clipboard!")
+ end
+ else
+ toplevel.display_status("Append a node into the root first!")
+ end
+ end
+
+ # Paste the data in the clipboard into the selected Array inserting it
+ # before the selected element.
+ def paste_node_inserting_before(item)
+ if current = selection.selected
+ if @clipboard_data
+ parent = current.parent or return
+ parent_type = parent.type
+ if parent_type == 'Array'
+ selected_index = parent.each_with_index do |c, i|
+ break i if c == current
+ end
+ Editor.data2model(@clipboard_data, model, parent) do |m|
+ m.insert_before(parent, current)
+ end
+ expand_collapse(current)
+ toplevel.display_status("Inserted an element to " +
+ "'#{parent_type}' before index #{selected_index}.")
+ window.change
+ else
+ toplevel.display_status(
+ "Cannot insert node below '#{parent_type}'!")
+ end
+ else
+ toplevel.display_status("Nothing to paste in clipboard!")
+ end
+ else
+ toplevel.display_status("Append a node into the root first!")
+ end
+ end
+
+ # Append a new node to the selected Hash or Array.
+ def append_new_node(item)
+ if parent = selection.selected
+ parent_type = parent.type
+ case parent_type
+ when 'Hash'
+ key, type, content = ask_for_hash_pair(parent)
+ key or return
+ iter = create_node(parent, 'Key', key)
+ iter = create_node(iter, type, content)
+ toplevel.display_status(
+ "Added a (key, value)-pair to '#{parent_type}'.")
+ window.change
+ when 'Array'
+ type, content = ask_for_element(parent)
+ type or return
+ iter = create_node(parent, type, content)
+ window.change
+ toplevel.display_status("Appendend an element to '#{parent_type}'.")
+ else
+ toplevel.display_status("Cannot append to '#{parent_type}'!")
+ end
+ else
+ type, content = ask_for_element
+ type or return
+ iter = create_node(nil, type, content)
+ window.change
+ end
+ end
+
+ # Insert a new node into an Array before the selected element.
+ def insert_new_node(item)
+ if current = selection.selected
+ parent = current.parent or return
+ parent_parent = parent.parent
+ parent_type = parent.type
+ if parent_type == 'Array'
+ selected_index = parent.each_with_index do |c, i|
+ break i if c == current
+ end
+ type, content = ask_for_element(parent)
+ type or return
+ iter = model.insert_before(parent, current)
+ iter.type, iter.content = type, content
+ toplevel.display_status("Inserted an element to " +
+ "'#{parent_type}' before index #{selected_index}.")
+ window.change
+ else
+ toplevel.display_status(
+ "Cannot insert node below '#{parent_type}'!")
+ end
+ else
+ toplevel.display_status("Append a node into the root first!")
+ end
+ end
+
+ # Recursively collapse/expand a subtree starting from the selected node.
+ def collapse_expand(item)
+ if current = selection.selected
+ if row_expanded?(current.path)
+ collapse_row(current.path)
+ else
+ expand_row(current.path, true)
+ end
+ else
+ toplevel.display_status("Append a node into the root first!")
+ end
+ end
+
+ # Create the menu.
+ def create
+ add_item("Change node", ?n, &method(:change_node))
+ add_separator
+ add_item("Cut node", ?X, &method(:cut_node))
+ add_item("Copy node", ?C, &method(:copy_node))
+ add_item("Paste node (appending)", ?A, &method(:paste_node_appending))
+ add_item("Paste node (inserting before)", ?I,
+ &method(:paste_node_inserting_before))
+ add_separator
+ add_item("Append new node", ?a, &method(:append_new_node))
+ add_item("Insert new node before", ?i, &method(:insert_new_node))
+ add_separator
+ add_item("Collapse/Expand node (recursively)", ?e,
+ &method(:collapse_expand))
+
+ menu.show_all
+ signal_connect(:button_press_event) do |widget, event|
+ if event.kind_of? Gdk::EventButton and event.button == 3
+ menu.popup(nil, nil, event.button, event.time)
+ end
+ end
+ signal_connect(:popup_menu) do
+ menu.popup(nil, nil, 0, Gdk::Event::CURRENT_TIME)
+ end
+ end
+ end
+
+ # This class creates the File pulldown menu.
+ class FileMenu
+ include MenuExtension
+
+ # Clear the model and filename, but ask to save the JSON document, if
+ # unsaved changes have occured.
+ def new(item)
+ window.clear
+ end
+
+ # Open a file and load it into the editor. Ask to save the JSON document
+ # first, if unsaved changes have occured.
+ def open(item)
+ window.file_open
+ end
+
+ def open_location(item)
+ window.location_open
+ end
+
+ # Revert the current JSON document in the editor to the saved version.
+ def revert(item)
+ window.instance_eval do
+ @filename and file_open(@filename)
+ end
+ end
+
+ # Save the current JSON document.
+ def save(item)
+ window.file_save
+ end
+
+ # Save the current JSON document under the given filename.
+ def save_as(item)
+ window.file_save_as
+ end
+
+ # Quit the editor, after asking to save any unsaved changes first.
+ def quit(item)
+ window.quit
+ end
+
+ # Create the menu.
+ def create
+ title = MenuItem.new('File')
+ title.submenu = menu
+ add_item('New', &method(:new))
+ add_item('Open', ?o, &method(:open))
+ add_item('Open location', ?l, &method(:open_location))
+ add_item('Revert', &method(:revert))
+ add_separator
+ add_item('Save', ?s, &method(:save))
+ add_item('Save As', ?S, &method(:save_as))
+ add_separator
+ add_item('Quit', ?q, &method(:quit))
+ title
+ end
+ end
+
+ # This class creates the Edit pulldown menu.
+ class EditMenu
+ include MenuExtension
+
+ # Copy data from model into primary clipboard.
+ def copy(item)
+ data = Editor.model2data(model.iter_first)
+ json = JSON.pretty_generate(data, :max_nesting => false)
+ c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
+ c.text = json
+ end
+
+ # Copy json text from primary clipboard into model.
+ def paste(item)
+ c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
+ if json = c.wait_for_text
+ window.ask_save if @changed
+ begin
+ window.edit json
+ rescue JSON::ParserError
+ window.clear
+ end
+ end
+ end
+
+ # Find a string in all nodes' contents and select the found node in the
+ # treeview.
+ def find(item)
+ @search = ask_for_find_term(@search) or return
+ iter = model.get_iter('0') or return
+ iter.recursive_each do |i|
+ if @iter
+ if @iter != i
+ next
+ else
+ @iter = nil
+ next
+ end
+ elsif @search.match(i[CONTENT_COL])
+ set_cursor(i.path, nil, false)
+ @iter = i
+ break
+ end
+ end
+ end
+
+ # Repeat the last search given by #find.
+ def find_again(item)
+ @search or return
+ iter = model.get_iter('0')
+ iter.recursive_each do |i|
+ if @iter
+ if @iter != i
+ next
+ else
+ @iter = nil
+ next
+ end
+ elsif @search.match(i[CONTENT_COL])
+ set_cursor(i.path, nil, false)
+ @iter = i
+ break
+ end
+ end
+ end
+
+ # Sort (Reverse sort) all elements of the selected array by the given
+ # expression. _x_ is the element in question.
+ def sort(item)
+ if current = selection.selected
+ if current.type == 'Array'
+ parent = current.parent
+ ary = Editor.model2data(current)
+ order, reverse = ask_for_order
+ order or return
+ begin
+ block = eval "lambda { |x| #{order} }"
+ if reverse
+ ary.sort! { |a,b| block[b] <=> block[a] }
+ else
+ ary.sort! { |a,b| block[a] <=> block[b] }
+ end
+ rescue => e
+ Editor.error_dialog(self, "Failed to sort Array with #{order}: #{e}!")
+ else
+ Editor.data2model(ary, model, parent) do |m|
+ m.insert_before(parent, current)
+ end
+ model.remove(current)
+ expand_collapse(parent)
+ window.change
+ toplevel.display_status("Array has been sorted.")
+ end
+ else
+ toplevel.display_status("Only Array nodes can be sorted!")
+ end
+ else
+ toplevel.display_status("Select an Array to sort first!")
+ end
+ end
+
+ # Create the menu.
+ def create
+ title = MenuItem.new('Edit')
+ title.submenu = menu
+ add_item('Copy', ?c, &method(:copy))
+ add_item('Paste', ?v, &method(:paste))
+ add_separator
+ add_item('Find', ?f, &method(:find))
+ add_item('Find Again', ?g, &method(:find_again))
+ add_separator
+ add_item('Sort', ?S, &method(:sort))
+ title
+ end
+ end
+
+ class OptionsMenu
+ include MenuExtension
+
+ # Collapse/Expand all nodes by default.
+ def collapsed_nodes(item)
+ if expanded
+ self.expanded = false
+ collapse_all
+ else
+ self.expanded = true
+ expand_all
+ end
+ end
+
+ # Toggle pretty saving mode on/off.
+ def pretty_saving(item)
+ @pretty_item.toggled
+ window.change
+ end
+
+ attr_reader :pretty_item
+
+ # Create the menu.
+ def create
+ title = MenuItem.new('Options')
+ title.submenu = menu
+ add_item('Collapsed nodes', nil, CheckMenuItem, &method(:collapsed_nodes))
+ @pretty_item = add_item('Pretty saving', nil, CheckMenuItem,
+ &method(:pretty_saving))
+ @pretty_item.active = true
+ window.unchange
+ title
+ end
+ end
+
+ # This class inherits from Gtk::TreeView, to configure it and to add a lot
+ # of behaviour to it.
+ class JSONTreeView < Gtk::TreeView
+ include Gtk
+
+ # Creates a JSONTreeView instance, the parameter _window_ is
+ # a MainWindow instance and used for self delegation.
+ def initialize(window)
+ @window = window
+ super(TreeStore.new(Gdk::Pixbuf, String, String))
+ self.selection.mode = SELECTION_BROWSE
+
+ @expanded = false
+ self.headers_visible = false
+ add_columns
+ add_popup_menu
+ end
+
+ # Returns the MainWindow instance of this JSONTreeView.
+ attr_reader :window
+
+ # Returns true, if nodes are autoexpanding, false otherwise.
+ attr_accessor :expanded
+
+ private
+
+ def add_columns
+ cell = CellRendererPixbuf.new
+ column = TreeViewColumn.new('Icon', cell,
+ 'pixbuf' => ICON_COL
+ )
+ append_column(column)
+
+ cell = CellRendererText.new
+ column = TreeViewColumn.new('Type', cell,
+ 'text' => TYPE_COL
+ )
+ append_column(column)
+
+ cell = CellRendererText.new
+ cell.editable = true
+ column = TreeViewColumn.new('Content', cell,
+ 'text' => CONTENT_COL
+ )
+ cell.signal_connect(:edited, &method(:cell_edited))
+ append_column(column)
+ end
+
+ def unify_key(iter, key)
+ return unless iter.type == 'Key'
+ parent = iter.parent
+ if parent.any? { |c| c != iter and c.content == key }
+ old_key = key
+ i = 0
+ begin
+ key = sprintf("%s.%d", old_key, i += 1)
+ end while parent.any? { |c| c != iter and c.content == key }
+ end
+ iter.content = key
+ end
+
+ def cell_edited(cell, path, value)
+ iter = model.get_iter(path)
+ case iter.type
+ when 'Key'
+ unify_key(iter, value)
+ toplevel.display_status('Key has been changed.')
+ when 'FalseClass'
+ value.downcase!
+ if value == 'true'
+ iter.type, iter.content = 'TrueClass', 'true'
+ end
+ when 'TrueClass'
+ value.downcase!
+ if value == 'false'
+ iter.type, iter.content = 'FalseClass', 'false'
+ end
+ when 'Numeric'
+ iter.content =
+ if value == 'Infinity'
+ value
+ else
+ (Integer(value) rescue Float(value) rescue 0).to_s
+ end
+ when 'String'
+ iter.content = value
+ when 'Hash', 'Array'
+ return
+ else
+ fail "Unknown type found in model: #{iter.type}"
+ end
+ window.change
+ end
+
+ def configure_value(value, type)
+ value.editable = false
+ case type
+ when 'Array', 'Hash'
+ value.text = ''
+ when 'TrueClass'
+ value.text = 'true'
+ when 'FalseClass'
+ value.text = 'false'
+ when 'NilClass'
+ value.text = 'null'
+ when 'Numeric', 'String'
+ value.text ||= ''
+ value.editable = true
+ else
+ raise ArgumentError, "unknown type '#{type}' encountered"
+ end
+ end
+
+ def add_popup_menu
+ menu = PopUpMenu.new(self)
+ menu.create
+ end
+
+ public
+
+ # Create a _type_ node with content _content_, and add it to _parent_
+ # in the model. If _parent_ is nil, create a new model and put it into
+ # the editor treeview.
+ def create_node(parent, type, content)
+ iter = if parent
+ model.append(parent)
+ else
+ new_model = Editor.data2model(nil)
+ toplevel.view_new_model(new_model)
+ new_model.iter_first
+ end
+ iter.type, iter.content = type, content
+ expand_collapse(parent) if parent
+ iter
+ end
+
+ # Ask for a hash key, value pair to be added to the Hash node _parent_.
+ def ask_for_hash_pair(parent)
+ key_input = type_input = value_input = nil
+
+ dialog = Dialog.new("New (key, value) pair for Hash", nil, nil,
+ [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
+ [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
+ )
+ dialog.width_request = 640
+
+ hbox = HBox.new(false, 5)
+ hbox.pack_start(Label.new("Key:"), false)
+ hbox.pack_start(key_input = Entry.new)
+ key_input.text = @key || ''
+ dialog.vbox.pack_start(hbox, false)
+ key_input.signal_connect(:activate) do
+ if parent.any? { |c| c.content == key_input.text }
+ toplevel.display_status('Key already exists in Hash!')
+ key_input.text = ''
+ else
+ toplevel.display_status('Key has been changed.')
+ end
+ end
+
+ hbox = HBox.new(false, 5)
+ hbox.pack_start(Label.new("Type:"), false)
+ hbox.pack_start(type_input = ComboBox.new(true))
+ ALL_TYPES.each { |t| type_input.append_text(t) }
+ type_input.active = @type || 0
+ dialog.vbox.pack_start(hbox, false)
+
+ type_input.signal_connect(:changed) do
+ value_input.editable = false
+ case ALL_TYPES[type_input.active]
+ when 'Array', 'Hash'
+ value_input.text = ''
+ when 'TrueClass'
+ value_input.text = 'true'
+ when 'FalseClass'
+ value_input.text = 'false'
+ when 'NilClass'
+ value_input.text = 'null'
+ else
+ value_input.text = ''
+ value_input.editable = true
+ end
+ end
+
+ hbox = HBox.new(false, 5)
+ hbox.pack_start(Label.new("Value:"), false)
+ hbox.pack_start(value_input = Entry.new)
+ value_input.width_chars = 60
+ value_input.text = @value || ''
+ dialog.vbox.pack_start(hbox, false)
+
+ dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
+ dialog.show_all
+ self.focus = dialog
+ dialog.run do |response|
+ if response == Dialog::RESPONSE_ACCEPT
+ @key = key_input.text
+ type = ALL_TYPES[@type = type_input.active]
+ content = value_input.text
+ return @key, type, content
+ end
+ end
+ return
+ ensure
+ dialog.destroy
+ end
+
+ # Ask for an element to be appended _parent_.
+ def ask_for_element(parent = nil, default_type = nil, value_text = @content)
+ type_input = value_input = nil
+
+ dialog = Dialog.new(
+ "New element into #{parent ? parent.type : 'root'}",
+ nil, nil,
+ [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
+ [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
+ )
+ hbox = HBox.new(false, 5)
+ hbox.pack_start(Label.new("Type:"), false)
+ hbox.pack_start(type_input = ComboBox.new(true))
+ default_active = 0
+ types = parent ? ALL_TYPES : CONTAINER_TYPES
+ types.each_with_index do |t, i|
+ type_input.append_text(t)
+ if t == default_type
+ default_active = i
+ end
+ end
+ type_input.active = default_active
+ dialog.vbox.pack_start(hbox, false)
+ type_input.signal_connect(:changed) do
+ configure_value(value_input, types[type_input.active])
+ end
+
+ hbox = HBox.new(false, 5)
+ hbox.pack_start(Label.new("Value:"), false)
+ hbox.pack_start(value_input = Entry.new)
+ value_input.width_chars = 60
+ value_input.text = value_text if value_text
+ configure_value(value_input, types[type_input.active])
+
+ dialog.vbox.pack_start(hbox, false)
+
+ dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
+ dialog.show_all
+ self.focus = dialog
+ dialog.run do |response|
+ if response == Dialog::RESPONSE_ACCEPT
+ type = types[type_input.active]
+ @content = case type
+ when 'Numeric'
+ if (t = value_input.text) == 'Infinity'
+ 1 / 0.0
+ else
+ Integer(t) rescue Float(t) rescue 0
+ end
+ else
+ value_input.text
+ end.to_s
+ return type, @content
+ end
+ end
+ return
+ ensure
+ dialog.destroy if dialog
+ end
+
+ # Ask for an order criteria for sorting, using _x_ for the element in
+ # question. Returns the order criterium, and true/false for reverse
+ # sorting.
+ def ask_for_order
+ dialog = Dialog.new(
+ "Give an order criterium for 'x'.",
+ nil, nil,
+ [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
+ [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
+ )
+ hbox = HBox.new(false, 5)
+
+ hbox.pack_start(Label.new("Order:"), false)
+ hbox.pack_start(order_input = Entry.new)
+ order_input.text = @order || 'x'
+ order_input.width_chars = 60
+
+ hbox.pack_start(reverse_checkbox = CheckButton.new('Reverse'), false)
+
+ dialog.vbox.pack_start(hbox, false)
+
+ dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
+ dialog.show_all
+ self.focus = dialog
+ dialog.run do |response|
+ if response == Dialog::RESPONSE_ACCEPT
+ return @order = order_input.text, reverse_checkbox.active?
+ end
+ end
+ return
+ ensure
+ dialog.destroy if dialog
+ end
+
+ # Ask for a find term to search for in the tree. Returns the term as a
+ # string.
+ def ask_for_find_term(search = nil)
+ dialog = Dialog.new(
+ "Find a node matching regex in tree.",
+ nil, nil,
+ [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
+ [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
+ )
+ hbox = HBox.new(false, 5)
+
+ hbox.pack_start(Label.new("Regex:"), false)
+ hbox.pack_start(regex_input = Entry.new)
+ hbox.pack_start(icase_checkbox = CheckButton.new('Icase'), false)
+ regex_input.width_chars = 60
+ if search
+ regex_input.text = search.source
+ icase_checkbox.active = search.casefold?
+ end
+
+ dialog.vbox.pack_start(hbox, false)
+
+ dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
+ dialog.show_all
+ self.focus = dialog
+ dialog.run do |response|
+ if response == Dialog::RESPONSE_ACCEPT
+ begin
+ return Regexp.new(regex_input.text, icase_checkbox.active? ? Regexp::IGNORECASE : 0)
+ rescue => e
+ Editor.error_dialog(self, "Evaluation of regex /#{regex_input.text}/ failed: #{e}!")
+ return
+ end
+ end
+ end
+ return
+ ensure
+ dialog.destroy if dialog
+ end
+
+ # Expand or collapse row pointed to by _iter_ according
+ # to the #expanded attribute.
+ def expand_collapse(iter)
+ if expanded
+ expand_row(iter.path, true)
+ else
+ collapse_row(iter.path)
+ end
+ end
+ end
+
+ # The editor main window
+ class MainWindow < Gtk::Window
+ include Gtk
+
+ def initialize(encoding)
+ @changed = false
+ @encoding = encoding
+ super(TOPLEVEL)
+ display_title
+ set_default_size(800, 600)
+ signal_connect(:delete_event) { quit }
+
+ vbox = VBox.new(false, 0)
+ add(vbox)
+ #vbox.border_width = 0
+
+ @treeview = JSONTreeView.new(self)
+ @treeview.signal_connect(:'cursor-changed') do
+ display_status('')
+ end
+
+ menu_bar = create_menu_bar
+ vbox.pack_start(menu_bar, false, false, 0)
+
+ sw = ScrolledWindow.new(nil, nil)
+ sw.shadow_type = SHADOW_ETCHED_IN
+ sw.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC)
+ vbox.pack_start(sw, true, true, 0)
+ sw.add(@treeview)
+
+ @status_bar = Statusbar.new
+ vbox.pack_start(@status_bar, false, false, 0)
+
+ @filename ||= nil
+ if @filename
+ data = read_data(@filename)
+ view_new_model Editor.data2model(data)
+ end
+
+ signal_connect(:button_release_event) do |_,event|
+ if event.button == 2
+ c = Gtk::Clipboard.get(Gdk::Selection::PRIMARY)
+ if url = c.wait_for_text
+ location_open url
+ end
+ false
+ else
+ true
+ end
+ end
+ end
+
+ # Creates the menu bar with the pulldown menus and returns it.
+ def create_menu_bar
+ menu_bar = MenuBar.new
+ @file_menu = FileMenu.new(@treeview)
+ menu_bar.append @file_menu.create
+ @edit_menu = EditMenu.new(@treeview)
+ menu_bar.append @edit_menu.create
+ @options_menu = OptionsMenu.new(@treeview)
+ menu_bar.append @options_menu.create
+ menu_bar
+ end
+
+ # Sets editor status to changed, to indicate that the edited data
+ # containts unsaved changes.
+ def change
+ @changed = true
+ display_title
+ end
+
+ # Sets editor status to unchanged, to indicate that the edited data
+ # doesn't containt unsaved changes.
+ def unchange
+ @changed = false
+ display_title
+ end
+
+ # Puts a new model _model_ into the Gtk::TreeView to be edited.
+ def view_new_model(model)
+ @treeview.model = model
+ @treeview.expanded = true
+ @treeview.expand_all
+ unchange
+ end
+
+ # Displays _text_ in the status bar.
+ def display_status(text)
+ @cid ||= nil
+ @status_bar.pop(@cid) if @cid
+ @cid = @status_bar.get_context_id('dummy')
+ @status_bar.push(@cid, text)
+ end
+
+ # Opens a dialog, asking, if changes should be saved to a file.
+ def ask_save
+ if Editor.question_dialog(self,
+ "Unsaved changes to JSON model. Save?")
+ if @filename
+ file_save
+ else
+ file_save_as
+ end
+ end
+ end
+
+ # Quit this editor, that is, leave this editor's main loop.
+ def quit
+ ask_save if @changed
+ if Gtk.main_level > 0
+ destroy
+ Gtk.main_quit
+ end
+ nil
+ end
+
+ # Display the new title according to the editor's current state.
+ def display_title
+ title = TITLE.dup
+ title << ": #@filename" if @filename
+ title << " *" if @changed
+ self.title = title
+ end
+
+ # Clear the current model, after asking to save all unsaved changes.
+ def clear
+ ask_save if @changed
+ @filename = nil
+ self.view_new_model nil
+ end
+
+ def check_pretty_printed(json)
+ pretty = !!((nl_index = json.index("\n")) && nl_index != json.size - 1)
+ @options_menu.pretty_item.active = pretty
+ end
+ private :check_pretty_printed
+
+ # Open the data at the location _uri_, if given. Otherwise open a dialog
+ # to ask for the _uri_.
+ def location_open(uri = nil)
+ uri = ask_for_location unless uri
+ uri or return
+ ask_save if @changed
+ data = load_location(uri) or return
+ view_new_model Editor.data2model(data)
+ end
+
+ # Open the file _filename_ or call the #select_file method to ask for a
+ # filename.
+ def file_open(filename = nil)
+ filename = select_file('Open as a JSON file') unless filename
+ data = load_file(filename) or return
+ view_new_model Editor.data2model(data)
+ end
+
+ # Edit the string _json_ in the editor.
+ def edit(json)
+ if json.respond_to? :read
+ json = json.read
+ end
+ data = parse_json json
+ view_new_model Editor.data2model(data)
+ end
+
+ # Save the current file.
+ def file_save
+ if @filename
+ store_file(@filename)
+ else
+ file_save_as
+ end
+ end
+
+ # Save the current file as the filename
+ def file_save_as
+ filename = select_file('Save as a JSON file')
+ store_file(filename)
+ end
+
+ # Store the current JSON document to _path_.
+ def store_file(path)
+ if path
+ data = Editor.model2data(@treeview.model.iter_first)
+ File.open(path + '.tmp', 'wb') do |output|
+ data or break
+ if @options_menu.pretty_item.active?
+ output.puts JSON.pretty_generate(data, :max_nesting => false)
+ else
+ output.write JSON.generate(data, :max_nesting => false)
+ end
+ end
+ File.rename path + '.tmp', path
+ @filename = path
+ toplevel.display_status("Saved data to '#@filename'.")
+ unchange
+ end
+ rescue SystemCallError => e
+ Editor.error_dialog(self, "Failed to store JSON file: #{e}!")
+ end
+
+ # Load the file named _filename_ into the editor as a JSON document.
+ def load_file(filename)
+ if filename
+ if File.directory?(filename)
+ Editor.error_dialog(self, "Try to select a JSON file!")
+ nil
+ else
+ @filename = filename
+ if data = read_data(filename)
+ toplevel.display_status("Loaded data from '#@filename'.")
+ end
+ display_title
+ data
+ end
+ end
+ end
+
+ # Load the data at location _uri_ into the editor as a JSON document.
+ def load_location(uri)
+ data = read_data(uri) or return
+ @filename = nil
+ toplevel.display_status("Loaded data from '#{uri}'.")
+ display_title
+ data
+ end
+
+ def parse_json(json)
+ check_pretty_printed(json)
+ if @encoding && !/^utf8$/i.match(@encoding)
+ json = JSON.iconv 'utf-8', @encoding, json
+ end
+ JSON::parse(json, :max_nesting => false, :create_additions => false)
+ end
+ private :parse_json
+
+ # Read a JSON document from the file named _filename_, parse it into a
+ # ruby data structure, and return the data.
+ def read_data(filename)
+ open(filename) do |f|
+ json = f.read
+ return parse_json(json)
+ end
+ rescue => e
+ Editor.error_dialog(self, "Failed to parse JSON file: #{e}!")
+ return
+ end
+
+ # Open a file selecton dialog, displaying _message_, and return the
+ # selected filename or nil, if no file was selected.
+ def select_file(message)
+ filename = nil
+ fs = FileSelection.new(message)
+ fs.set_modal(true)
+ @default_dir = File.join(Dir.pwd, '') unless @default_dir
+ fs.set_filename(@default_dir)
+ fs.set_transient_for(self)
+ fs.signal_connect(:destroy) { Gtk.main_quit }
+ fs.ok_button.signal_connect(:clicked) do
+ filename = fs.filename
+ @default_dir = File.join(File.dirname(filename), '')
+ fs.destroy
+ Gtk.main_quit
+ end
+ fs.cancel_button.signal_connect(:clicked) do
+ fs.destroy
+ Gtk.main_quit
+ end
+ fs.show_all
+ Gtk.main
+ filename
+ end
+
+ # Ask for location URI a to load data from. Returns the URI as a string.
+ def ask_for_location
+ dialog = Dialog.new(
+ "Load data from location...",
+ nil, nil,
+ [ Stock::OK, Dialog::RESPONSE_ACCEPT ],
+ [ Stock::CANCEL, Dialog::RESPONSE_REJECT ]
+ )
+ hbox = HBox.new(false, 5)
+
+ hbox.pack_start(Label.new("Location:"), false)
+ hbox.pack_start(location_input = Entry.new)
+ location_input.width_chars = 60
+ location_input.text = @location || ''
+
+ dialog.vbox.pack_start(hbox, false)
+
+ dialog.signal_connect(:'key-press-event', &DEFAULT_DIALOG_KEY_PRESS_HANDLER)
+ dialog.show_all
+ dialog.run do |response|
+ if response == Dialog::RESPONSE_ACCEPT
+ return @location = location_input.text
+ end
+ end
+ return
+ ensure
+ dialog.destroy if dialog
+ end
+ end
+
+ class << self
+ # Starts a JSON Editor. If a block was given, it yields
+ # to the JSON::Editor::MainWindow instance.
+ def start(encoding = 'utf8') # :yield: window
+ Gtk.init
+ @window = Editor::MainWindow.new(encoding)
+ @window.icon_list = [ Editor.fetch_icon('json') ]
+ yield @window if block_given?
+ @window.show_all
+ Gtk.main
+ end
+
+ # Edit the string _json_ with encoding _encoding_ in the editor.
+ def edit(json, encoding = 'utf8')
+ start(encoding) do |window|
+ window.edit json
+ end
+ end
+
+ attr_reader :window
+ end
+ end
+end
View
28 deploy/lib/json/ext.rb
@@ -0,0 +1,28 @@
+require 'json/common'
+
+module JSON
+ # This module holds all the modules/classes that implement JSON's
+ # functionality as C extensions.
+ module Ext
+ begin
+ if defined?(RUBY_ENGINE) == 'constant' and RUBY_ENGINE == 'ruby' and RUBY_VERSION =~ /\A1\.9\./
+ require 'json/ext/1.9/parser'
+ require 'json/ext/1.9/generator'
+ elsif !defined?(RUBY_ENGINE) && RUBY_VERSION =~ /\A1\.8\./
+ require 'json/ext/1.8/parser'
+ require 'json/ext/1.8/generator'
+ else