require 'much-plugin' require 'mr/json_field/fake_record' require 'mr/json_field/record' module MR module JsonField include MuchPlugin DEFAULT_ENCODER = proc{ |value| ::JSON.dump(value) } DEFAULT_DECODER = proc{ |value| ::JSON.load(value) } def self.encoder; @encoder ||= DEFAULT_ENCODER; end def self.encoder=(new_value); @encoder = new_value; end def self.decoder; @decoder ||= DEFAULT_DECODER; end def self.decoder=(new_value); @decoder = new_value; end def self.encode(value) self.encoder.call(value) rescue StandardError => exception raise InvalidJSONError, exception.message end def self.decode(value) self.decoder.call(value) rescue StandardError => exception raise InvalidJSONError, exception.message end # this can be used with `MR::Model` or `MR::ReadModel`, so it doesn't # include either by default plugin_included do extend ClassMethods end module ClassMethods def json_field(field_name, options = nil) json_field_reader(field_name, options) json_field_writer(field_name, options) end def json_field_reader(field_name, options = nil) options ||= {} field_name = field_name.to_s ivar_name = "@#{field_name}" source_field_name = (options[:source] || "#{field_name}_json").to_s if source_field_name == field_name raise ArgumentError, "the field name and source cannot be the same" end define_method(field_name) do if !(cached_value = self.instance_variable_get(ivar_name)).nil? return cached_value else source_value = self.send(source_field_name) return source_value if source_value.nil? value = begin MR::JsonField.decode(source_value) rescue InvalidJSONError => exception message = "can't decode `#{field_name}` JSON field (from " \ "`#{source_field_name}`): #{exception.message}" raise exception.class, message, caller end # cache the decoded value in the ivar self.instance_variable_set(ivar_name, value) value end end (self.json_field_readers << field_name).uniq! (self.json_field_source_fields << source_field_name).uniq! end def json_field_writer(field_name, options = nil) options ||= {} field_name = field_name.to_s.strip ivar_name = "@#{field_name}" source_field_name = (options[:source] || "#{field_name}_json").to_s.strip if source_field_name == field_name raise ArgumentError, "the field name and source cannot be the same" end define_method("#{field_name}=") do |new_value| encoded_value = if !new_value.nil? begin MR::JsonField.encode(new_value) rescue InvalidJSONError => exception message = "can't encode value for `#{field_name}` JSON field: " \ "#{exception.message}" raise exception.class, message, caller end else nil end self.send("#{source_field_name}=", encoded_value) # reset the ivar so its value will be calculated again when read self.instance_variable_set(ivar_name, nil) new_value end (self.json_field_writers << field_name).uniq! (self.json_field_source_fields << source_field_name).uniq! end def json_field_readers; @json_field_readers ||= []; end def json_field_writers; @json_field_writers ||= []; end def json_field_accessors self.json_field_readers & self.json_field_writers end def json_field_source_fields @json_field_source_fields ||= [] end end module TestHelpers include MuchPlugin plugin_included do include InstanceMethods require 'mr/factory' end module InstanceMethods def assert_json_field(subject, field_name, options = nil) assert_json_field_reader(subject, field_name, options) assert_json_field_writer(subject, field_name, options) end def assert_json_field_reader(subject, field_name, options = nil) options ||= {} source_field_name = options[:source] || "#{field_name}_json" # set a value to read if it's `nil` if subject.send(source_field_name).nil? encoded_value = MR::JsonField.encode({ MR::Factory.string => MR::Factory.string }) subject.send("#{source_field_name}=", encoded_value) end assert_respond_to "#{field_name}", subject exp = MR::JsonField.decode(subject.send"#{source_field_name}") assert_equal exp, subject.send("#{field_name}") end def assert_json_field_writer(subject, field_name, options = nil) options ||= {} source_field_name = options[:source] || "#{field_name}_json" assert_respond_to "#{field_name}=", subject new_value = { MR::Factory.string => MR::Factory.string } subject.send("#{field_name}=", new_value) exp = MR::JsonField.encode(new_value) assert_equal exp, subject.send("#{source_field_name}") end end end InvalidJSONError = Class.new(StandardError) end end