# Wx::SF::Serializer - shape serializer module # Copyright (c) M.J.N. Corino, The Netherlands require 'json' require 'json/add/core' require 'json/add/set' require 'json/add/ostruct' module Wx::SF module Serializable module JSON # Derived Hash class to use for deserialized JSON object data which # supports using Symbol keys. class ObjectHash < ::Hash # Returns the object associated with given key. # @param [String,Symbol] key key value # @return [Object] associated object # @see ::Hash#[] def [](key) super(key.to_s) end # Returns true if the given key exists in self otherwise false. # @param [String,Symbol] key key value # @return [Boolean] # @see ::Hash#include? def include?(key) super(key.to_s) end alias member? include? alias has_key? include? alias key? include? end # Mixin module to patch hash objects during JSON serialization. # By default JSON will not consider hash keys for custom serialization # but assumes any key should be serialized as it's string representation. # This is restrictive but compatible with "pure" JSON object notation. # JSON however also does not (correctly?) honour overriding Hash#to_json to # customize serialization of Hash-es which seems too restrictive (stupid?) # as using more complex custom keys for Hash-es instead of String/Symbol-s # is not that uncommon. # This mixin is used to "patch" Hash **instances** through #extend. module HashInstancePatch def patch_nested_hashes(obj) case obj when ::Hash obj.extend(HashInstancePatch) unless obj.singleton_class.include?(HashInstancePatch) obj.each_pair { |k, v| patch_nested_hashes(k); patch_nested_hashes(v) } when ::Array obj.each { |e| patch_nested_hashes(e) } end obj end private :patch_nested_hashes # Returns JSON representation (String) of self. # Hash data which is part of object properties/members being serialized # (including any nested Hash-es) will be patched with HashInstancePatch. # Patched Hash instances will be serialized as JSON-creatable objects # (so provided with a JSON#create_id) with the hash contents represented # as an array of key/value pairs (arrays). # @param [Array] args any args passed by the JSON generator # @return [String] JSON representation def to_json(*args) if self.has_key?(::JSON.create_id) if self.has_key?('data') if (data = self['data']).is_a?(::Hash) data.each_value { |v| patch_nested_hashes(v) } end else # core class extensions use different data members for property serialization self.each_value { |v| patch_nested_hashes(v) } end super else { ::JSON.create_id => self.class.name, 'data' => patch_nested_hashes(to_a) }.to_json(*args) end end end # Mixin module to patch singleton_clas of the Hash class to make Hash-es # JSON creatable (#json_creatable? returns true). module HashClassPatch # Create a new Hash instance from deserialized JSON data. # @param [Hash] object deserialized JSON object # @return [Hash] restored Hash instance def json_create(object) object['data'].to_h end end class ::Hash include Wx::SF::Serializable::JSON::HashInstancePatch class << self include Wx::SF::Serializable::JSON::HashClassPatch end end class << self def serializables ::Set.new [::NilClass, ::TrueClass, ::FalseClass, ::Integer, ::Float, ::String, ::Array, ::Hash, ::Date, ::DateTime, ::Exception, ::Range, ::Regexp, ::Struct, ::Symbol, ::Time, ::Set, ::OpenStruct] end TLS_SAFE_DESERIALIZE_KEY = :wx_sf_json_safe_deserialize.freeze private_constant :TLS_SAFE_DESERIALIZE_KEY TLS_PARSE_STACK_KEY = :wx_sf_json_parse_stack.freeze private_constant :TLS_PARSE_STACK_KEY def safe_deserialize ::Thread.current[TLS_SAFE_DESERIALIZE_KEY] ||= [] end private :safe_deserialize def start_safe_deserialize safe_deserialize.push(true) end def end_safe_deserialize safe_deserialize.pop end def parse_stack ::Thread.current[TLS_PARSE_STACK_KEY] ||= [] end private :parse_stack def start_parse parse_stack.push(safe_deserialize.pop) end def end_parse unless (val = parse_stack.pop).nil? safe_deserialize.push(val) end end def safe_parsing? !!parse_stack.last end end def self.dump(obj, io=nil, pretty: false) obj.extend(HashInstancePatch) if obj.is_a?(::Hash) if pretty if io || io.respond_to?(:write) io.write(::JSON.pretty_generate(obj)) io else ::JSON.pretty_generate(obj) end else ::JSON.dump(obj, io) end end def self.load(source) begin # initialize ID restoration map Serializable::ID.init_restoration_map # enable safe deserializing self.start_safe_deserialize ::JSON.parse!(source, **{create_additions: true, object_class: Serializable::JSON::ObjectHash}) ensure # reset safe deserializing self.end_safe_deserialize # reset ID restoration map Serializable::ID.clear_restoration_map end end end # extend serialization class methods module SerializeClassMethods def json_create(object) create_for_deserialize(data = object['data']) .__send__(:from_serialized, data) end end # extend instance serialization methods module SerializeInstanceMethods def to_json(*args) { ::JSON.create_id => self.class.name, 'data' => for_serialize(Hash.new) }.to_json(*args) end end class ID def self.json_create(object) create_for_deserialize(data = object['data']) end def to_json(*args) { ::JSON.create_id => self.class.name, 'data' => for_serialize(Hash.new) }.to_json(*args) end end register(Serializable.default_format, JSON) end end module ::JSON class << self alias :pre_wxsf_parse! :parse! def parse!(*args, **kwargs) begin # setup parsing stack for safe or normal deserializing # the double bracketing provided from Wx::SF::Serializable::JSON#load and here # makes sure to support both nested Wx::SF deserializing as well as nested # hybrid deserializing (Wx::SF -> common JSON -> ...) Wx::SF::Serializable::JSON.start_parse pre_wxsf_parse!(*args, **kwargs) ensure # reset parsing stack Wx::SF::Serializable::JSON.end_parse end end end end class ::Class # override this to be able to do safe deserializing def json_creatable? if Wx::SF::Serializable::JSON.safe_parsing? return false unless Wx::SF::Serializable::JSON.serializables.include?(self) || Wx::SF::Serializable.serializables.include?(self) || ::Struct > self end respond_to?(:json_create) end end