# Wx::SF::Serializer - shape serializer module # Copyright (c) M.J.N. Corino, The Netherlands require 'set' module Wx::SF module Serializable class Property def initialize(klass, prop, proc=nil, force: false, handler: nil, &block) ::Kernel.raise ArgumentError, "Invalid property id [#{prop}]" unless ::String === prop || ::Symbol === prop ::Kernel.raise ArgumentError, "Duplicate property id [#{prop}]" if klass.has_serializer_property?(prop) @klass = klass @id = prop.to_sym @forced = force if block || handler if handler ::Kernel.raise ArgumentError, "Invalid property handler #{handler} for #{prop}" unless ::Proc === handler || ::Symbol === handler if handler.is_a?(::Proc) ::Kernel.raise ArgumentError, "Invalid property block #{proc} for #{prop}" unless block.arity == -3 @getter = ->(obj) { handler.call(@id, obj) } @setter = ->(obj, val) { handler.call(@id, obj, val) } else @getter = ->(obj) { obj.send(handler, @id) } @setter = ->(obj, val) { obj.send(handler, @id, val) } end else # any property block MUST accept 2 or 3 args; property name, instance and value (for setter) ::Kernel.raise ArgumentError, "Invalid property block #{proc} for #{prop}" unless block.arity == -3 @getter = ->(obj) { block.call(@id, obj) } @setter = ->(obj, val) { block.call(@id, obj, val) } end elsif proc ::Kernel.raise ArgumentError, "Invalid property proc #{proc} for #{prop}" unless ::Proc === proc || ::Symbol === proc if ::Proc === proc # any property proc should be callable with a single arg (instance) @getter = proc # a property proc combining getter/setter functionality should accept a single or more args (instance + value) @setter = (proc.arity == -2) ? proc : nil else @getter = ->(obj) { obj.send(proc) } @setter = ->(obj, val) { obj.send(proc, val) } end end end attr_reader :id def serialize(obj, data, excludes) unless excludes.include?(@id) val = getter.call(obj) unless Serializable === val && val.serialize_disabled? && !@forced data[@id] = case val when ::Array val.select { |elem| !(Serializable === elem && elem.serialize_disabled?) } when ::Set ::Set.new(val.select { |elem| !(Serializable === elem && elem.serialize_disabled?) }) else val end end end end def deserialize(obj, data) if data.has_key?(@id) setter.call(obj, data[@id]) end end def get(obj) getter.call(obj) end def get_method(id) begin @klass.instance_method(id) rescue NameError nil end end private :get_method def getter unless @getter inst_meth = get_method(@id) inst_meth = get_method("get_#{@id}") unless inst_meth if inst_meth @getter = ->(obj) { inst_meth.bind(obj).call } else return self.method(:getter_fail) end end @getter end private :getter def setter unless @setter inst_meth = get_method("#{@id}=") inst_meth = get_method("set_#{@id}") unless inst_meth unless inst_meth im = get_method(@id) if im && im.arity == -1 inst_meth = im else inst_meth = nil end end if inst_meth @setter = ->(obj, val) { inst_meth.bind(obj).call(val) } else return self.method(:setter_noop) end end @setter end private :setter def getter_fail(obj) ::Kernel.raise RuntimeError, "Missing getter for property #{@id} of #{@klass}" end private :getter_fail def setter_noop(_, _) # do nothing end private :setter_noop end # Serializable unique ids. # This class makes sure to maintain uniqueness across serialization/deserialization cycles # and keeps all shared instances within a single (serialized/deserialized) object set in # sync. class ID; end class << self def serializables @serializables ||= ::Set.new end def formatters @formatters ||= {} end private :formatters # Registers a serialization formatting engine # @param [Symbol,String] format format id # @param [Object] engine formatting engine def register(format, engine) if formatters.has_key?(format.to_s.downcase) ::Kernel.raise ArgumentError, "Duplicate serialization formatter registration for #{format}" end formatters[format.to_s.downcase] = engine end # Return a serialization formatting engine # @param [Symbol,String] format format id # @return [Object] formatting engine def [](format) formatters[format.to_s.downcase] end def default_format @default_format ||= :json end def default_format=(format) @default_format = format end end # Mixin module for classes that get Wx::SF::Serializable included. # This module is used to extend the class methods of the serializable class. module SerializeClassMethods # Adds (a) serializable property(-ies) for instances of his class (and derived classes) # @overload property(*props, force: false) # Specifies one or more serialized properties. # The serialization framework will determine the availability of setter and getter methods # automatically by looking for methods "#{prop_id}=(v)", "set_#{prop_id}(v)" or "#{prop}(v)" # for setters and "#{prop_id}()" or "get_#{prop_id}" for getters. # @param [Symbol,String] props one or more ids of serializable properties # @param [Boolean] force overrides any #disable_serialize for the properties specified # @overload property(hash, force: false) # Specifies one or more serialized properties with associated setter/getter method ids/procs/lambda-s. # @example # property( # prop_a: ->(obj, *val) { # obj.my_prop_a_setter(val.first) unless val.empty? # obj.my_prop_a_getter # }, # prop_b: Proc.new { |obj, *val| # obj.my_prop_b_setter(val.first) unless val.empty? # obj.my_prop_b_getter # }, # prop_c: :serialization_method) # Procs with setter support MUST accept 1 or 2 arguments (1 for getter, 2 for setter). # @note Use `*val` to specify the optional value argument for setter requests instead of `val=nil` # to be able to support setting explicit nil values. # @param [Hash] hash a hash of pairs of property ids and getter/setter procs # @param [Boolean] force overrides any #disable_serialize for the properties specified # @overload property(*props, force: false, handler: nil, &block) # Specifies one or more serialized properties with a getter/setter handler proc/method/block. # The getter/setter proc or block should accept either 2 (property id and object for getter) or 3 arguments # (property id, object and value for setter) and is assumed to handle getter/setter requests # for all specified properties. # The getter/setter method should accept either 1 (property id for getter) or 2 arguments # (property id and value for setter) and is assumed to handle getter/setter requests # for all specified properties. # @example # property(:property_a, :property_b, :property_c) do |id, obj, *val| # case id # when :property_a # ... # when :property_b # ... # when :property_c # ... # end # end # @note Use `*val` to specify the optional value argument for setter requests instead of `val=nil` # to be able to support setting explicit nil values. # @param [Symbol,String] props one or more ids of serializable properties # @param [Boolean] force overrides any #disable_serialize for the properties specified # @yieldparam [Symbol,String] id property id # @yieldparam [Object] obj object instance # @yieldparam [Object] val optional property value to set in case of setter request def property(*props, **kwargs, &block) forced = !!kwargs.delete(:force) if block || kwargs[:handler] props.each do |prop| serializer_properties << Property.new(self, prop, force: forced, handler: kwargs[:handler], &block) end else props.flatten.each do |prop| if ::Hash === prop prop.each_pair do |pn, pp| serializer_properties << Property.new(self, pn, pp, force: forced) end else serializer_properties << Property.new(self, prop, force: forced) end end unless kwargs.empty? kwargs.each_pair do |pn, pp| serializer_properties << Property.new(self, pn, pp, force: forced) end end end end alias :properties :property alias :contains :property # excludes a serializable property for instances of this class # (mostly/only useful to exclude properties from base classes which # do not require serialization for derived class) def excluded_property(*props) excluded_serializer_properties.merge props.flatten.collect { |prop| prop.to_s } end alias :excluded_properties :excluded_property alias :excludes :excluded_property # Creates a new instance for subsequent deserialization and optionally initialize # it using the given data hash. # The default implementation creates a new instance using the default constructor # (no arguments, no initialization) and leaves the initialization to a subsequent call # to the instance method #from_serialized(data). # Classes that do not support a default constructor can override this class method and # implement a custom creation scheme. # @param [Hash] _data hash containing deserialized property data (symbol keys) # @return [Object] the newly created object def create_for_deserialize(_data) self.new end end # Mixin module for classes that get Wx::SF::Serializable included. # This module is used to extend the instance methods of the serializable class. module SerializeInstanceMethods # Serialize this object # @overload serialize(pretty: false, format: Serializable.default_format) # @param [Boolean] pretty if true specifies to generate pretty formatted output if possible # @param [Symbol,String] format specifies output format # @return [String] serialized data # @overload serialize(io, pretty: false, format: Serializable.default_format) # @param [IO] io output stream to write serialized data to # @param [Boolean] pretty if true specifies to generate pretty formatted output if possible # @param [Symbol,String] format specifies output format # @return [IO] def serialize(io = nil, pretty: false, format: Serializable.default_format) Serializable[format].dump(self, io, pretty: pretty) end # Returns true if regular serialization for this object has been disabled, false otherwise (default). # Disabled serialization can be overridden for single objects (not objects maintained in property containers # like arrays and sets). # @return [Boolean] def serialize_disabled? !!@serialize_disabled # true for any value but false end # Disables serialization for this object as a single property or as part of a property container # (array or set). # @return [void] def disable_serialize # by default unset (nil) so serializing enabled @serialize_disabled = true end # @!method for_serialize(hash, excludes = Set.new) # Serializes the properties of a serializable instance to the given hash # except when the property id is included in excludes. # @param [Hash] hash property serialization hash # @param [Set] excludes set with excluded property ids # @return [Hash] property serialization hash # @!method from_serialized(hash) # Restores the properties of a deserialized instance. # @param [Hash] hash deserialized properties hash # @return [self] end # Serialize the given object # @overload serialize(obj, pretty: false, format: Serializable.default_format) # @param [Object] obj object to serialize # @param [Boolean] pretty if true specifies to generate pretty formatted output if possible # @param [Symbol,String] format specifies output format # @return [String] serialized data # @overload serialize(obj, io, pretty: false, format: Serializable.default_format) # @param [Object] obj object to serialize # @param [IO] io output stream to write serialized data to # @param [Boolean] pretty if true specifies to generate pretty formatted output if possible # @param [Symbol,String] format specifies output format # @return [IO] def self.serialize(obj, io = nil, pretty: false, format: Serializable.default_format) self[format].dump(obj, io, pretty: pretty) end # Deserializes object from source data # @param [IO,String] source source data (stream) # @param [Symbol, String] format data format of source # @return [Object] deserialized object def self.deserialize(source, format: Serializable.default_format) self[format].load(::IO === source || source.respond_to?(:read) ? source.read : source) end def self.included(base) ::Kernel.raise RuntimeError, "#{self} should only be included in classes" if base.instance_of?(::Module) # register as serializable class Serializable.serializables << base return if base == Serializable::ID # special case which does not need the rest # provide serialized property definition support # provide serialized classes with their own serialized properties (exclusion) list base.singleton_class.class_eval do def serializer_properties @serializer_props ||= [] end def excluded_serializer_properties @excluded_serializer_props ||= ::Set.new end end # add class methods base.extend(SerializeClassMethods) # add instance property (de-)serialization methods for base class base.class_eval <<~__CODE def for_serialize(hash, excludes = ::Set.new) hash[:'@explicit'] = true if serialize_disabled? # mark explicit serialize overriding disabling #{base.name}.serializer_properties.each { |prop, h| prop.serialize(self, hash, excludes) } hash end protected :for_serialize def from_serialized(hash) disable_serialize if hash[:'@explicit'] # re-instate serialization disabling #{base.name}.serializer_properties.each { |prop| prop.deserialize(self, hash) } self end protected :from_serialized def self.has_serializer_property?(id) self.serializer_properties.any? { |p| p.id == id.to_sym } end __CODE # add inheritance support base.class_eval do def self.inherited(derived) # add instance property (de-)serialization methods for derived classes derived.class_eval <<~__CODE module SerializerMethods def for_serialize(hash, excludes = ::Set.new) hash = super(hash, excludes | #{derived.name}.excluded_serializer_properties) #{derived.name}.serializer_properties.each { |prop| prop.serialize(self, hash, excludes) } hash end protected :for_serialize def from_serialized(hash) #{derived.name}.serializer_properties.each { |prop| prop.deserialize(self, hash) } super(hash) end protected :from_serialized end include SerializerMethods __CODE derived.class_eval do def self.has_serializer_property?(id) self.serializer_properties.any? { |p| p.id == id.to_sym } || self.superclass.has_serializer_property?(id) end end # register as serializable class Serializable.serializables << derived end end # add instance serialization method base.include(SerializeInstanceMethods) end end # module Serializable end # module Wx::SF Dir[File.join(__dir__, 'serializer', '*.rb')].each { |fnm| require "wx/shapes/serializer/#{File.basename(fnm)}" } Dir[File.join(__dir__, 'serialize', '*.rb')].each { |fnm| require "wx/shapes/serialize/#{File.basename(fnm)}" }