require 'test_helper' class RepresentableTest < MiniTest::Spec class Band include Representable property :name attr_accessor :name end class PunkBand < Band property :street_cred attr_accessor :street_cred end module BandRepresentation include Representable property :name end module PunkBandRepresentation include Representable include BandRepresentation property :street_cred end describe "#representable_attrs" do it "responds to #representable_attrs" do assert_equal 1, Band.representable_attrs.size assert_equal "name", Band.representable_attrs.first.name end describe "in module" do it "returns definitions" do assert_equal 1, BandRepresentation.representable_attrs.size assert_equal "name", BandRepresentation.representable_attrs.first.name end it "inherits to including modules" do assert_equal 2, PunkBandRepresentation.representable_attrs.size assert_equal "name", PunkBandRepresentation.representable_attrs.first.name assert_equal "street_cred", PunkBandRepresentation.representable_attrs.last.name end it "inherits to including class" do band = Class.new do include Representable include PunkBandRepresentation end assert_equal 2, band.representable_attrs.size assert_equal "name", band.representable_attrs.first.name assert_equal "street_cred", band.representable_attrs.last.name end it "allows including the concrete representer module later" do vd = class VD attr_accessor :name, :street_cred include Representable::JSON include PunkBandRepresentation end.new vd.name = "Vention Dention" vd.street_cred = 1 assert_json "{\"name\":\"Vention Dention\",\"street_cred\":1}", vd.to_json end #it "allows including the concrete representer module only" do # require 'representable/json' # module RockBandRepresentation # include Representable::JSON # property :name # end # vd = class VH # include RockBandRepresentation # end.new # vd.name = "Van Halen" # assert_equal "{\"name\":\"Van Halen\"}", vd.to_json #end it "doesn't share inherited properties between family members" do parent = Module.new do include Representable property :id end child = Module.new do include Representable include parent end assert parent.representable_attrs.first != child.representable_attrs.first, "definitions shouldn't be identical" end end end describe "Representable" do describe "inheritance" do class CoverSong < OpenStruct end module SongRepresenter include Representable::Hash property :name end module CoverSongRepresenter include Representable::Hash include SongRepresenter property :by end it "merges properties from all ancestors" do props = {"name"=>"The Brews", "by"=>"Nofx"} assert_equal(props, CoverSong.new(props).extend(CoverSongRepresenter).to_hash) end end it "allows mixing in multiple representers" do require 'representable/json' require 'representable/xml' class Bodyjar include Representable::XML include Representable::JSON include PunkBandRepresentation self.representation_wrap = "band" attr_accessor :name, :street_cred end band = Bodyjar.new band.name = "Bodyjar" assert_json "{\"band\":{\"name\":\"Bodyjar\"}}", band.to_json assert_xml_equal "Bodyjar", band.to_xml end it "allows extending with different representers subsequentially" do module SongXmlRepresenter include Representable::XML property :name, :from => "name", :attribute => true end module SongJsonRepresenter include Representable::JSON property :name end @song = Song.new("Days Go By") assert_xml_equal "", @song.extend(SongXmlRepresenter).to_xml assert_json "{\"name\":\"Days Go By\"}", @song.extend(SongJsonRepresenter).to_json end end describe "#property" do describe ":from" do # TODO: do this with all options. it "can be set explicitly" do band = Class.new(Band) { property :friends, :from => :friend } assert_equal "friend", band.representable_attrs.last.from end it "can be set explicitly with as" do band = Class.new(Band) { property :friends, :as => :friend } assert_equal "friend", band.representable_attrs.last.from end it "is infered from the name implicitly" do band = Class.new(Band) { property :friends } assert_equal "friends", band.representable_attrs.last.from end end end describe "#collection" do class RockBand < Band collection :albums end it "creates correct Definition" do assert_equal "albums", RockBand.representable_attrs.last.name assert RockBand.representable_attrs.last.array? end end describe "#hash" do it "also responds to the original method" do assert_kind_of Integer, BandRepresentation.hash end end describe "#representation_wrap" do class HardcoreBand include Representable end class SoftcoreBand < HardcoreBand end before do @band = HardcoreBand.new end it "returns false per default" do assert_equal nil, SoftcoreBand.new.send(:representation_wrap) end it "infers a printable class name if set to true" do HardcoreBand.representation_wrap = true assert_equal "hardcore_band", @band.send(:representation_wrap) end it "can be set explicitely" do HardcoreBand.representation_wrap = "breach" assert_equal "breach", @band.send(:representation_wrap) end end describe "#definition_class" do it "returns Definition class" do assert_equal Representable::Definition, Band.send(:definition_class) end end # DISCUSS: i don't like the JSON requirement here, what about some generic test module? class PopBand include Representable::JSON property :name property :groupies attr_accessor :name, :groupies end describe "#update_properties_from" do before do @band = PopBand.new end it "copies values from document to object" do @band.update_properties_from({"name"=>"No One's Choice", "groupies"=>2}, {}, Representable::Hash::PropertyBinding) assert_equal "No One's Choice", @band.name assert_equal 2, @band.groupies end it "accepts :exclude option" do @band.update_properties_from({"name"=>"No One's Choice", "groupies"=>2}, {:exclude => [:groupies]}, Representable::Hash::PropertyBinding) assert_equal "No One's Choice", @band.name assert_equal nil, @band.groupies end it "accepts :include option" do @band.update_properties_from({"name"=>"No One's Choice", "groupies"=>2}, {:include => [:groupies]}, Representable::Hash::PropertyBinding) assert_equal 2, @band.groupies assert_equal nil, @band.name end it "ignores non-writeable properties" do @band = Class.new(Band) { property :name; collection :founders, :writeable => false; attr_accessor :founders }.new @band.update_properties_from({"name" => "Iron Maiden", "groupies" => 2, "founders" => [{ "name" => "Steve Harris" }] }, {}, Representable::Hash::PropertyBinding) assert_equal "Iron Maiden", @band.name assert_equal nil, @band.founders end it "always returns self" do assert_equal @band, @band.update_properties_from({"name"=>"Nofx"}, {}, Representable::Hash::PropertyBinding) end it "includes false attributes" do @band.update_properties_from({"groupies"=>false}, {}, Representable::Hash::PropertyBinding) assert_equal false, @band.groupies end it "ignores properties not present in the incoming document" do @band.instance_eval do def name=(*); raise "I should never be called!"; end end @band.update_properties_from({}, {}, Representable::Hash::PropertyBinding) end it "ignores (no-default) properties not present in the incoming document" do { Representable::JSON => [{}, Representable::Hash::PropertyBinding], Representable::XML => [xml(%{}), Representable::XML::PropertyBinding] }.each do |format, config| nested_repr = Module.new do # this module is never applied. include format property :created_at end repr = Module.new do include format property :name, :class => Object, :extend => nested_repr end @band = Band.new.extend(repr) @band.update_properties_from(config.first, {}, config.last) assert_equal nil, @band.name, "Failed in #{format}" end end describe "passing options" do module TrackRepresenter include Representable::Hash property :nr def to_hash(options) @nr = options[:nr] super end def from_hash(data, options) super @nr = options[:nr] end attr_accessor :nr end representer! do property :track, :extend => TrackRepresenter end describe "#to_hash" do it "propagates to nested objects" do Song.new("Ocean Song", Object.new).extend(representer).to_hash(:nr => 9).must_equal({"track"=>{"nr"=>9}}) end end describe "#from_hash" do it "propagates to nested objects" do Song.new.extend(representer).from_hash({"track"=>{"nr" => "replace me"}}, :nr => 9).track.must_equal 9 end end end end describe "#create_representation_with" do before do @band = PopBand.new @band.name = "No One's Choice" @band.groupies = 2 end it "compiles document from properties in object" do assert_equal({"name"=>"No One's Choice", "groupies"=>2}, @band.send(:create_representation_with, {}, {}, Representable::Hash::PropertyBinding)) end it "accepts :exclude option" do hash = @band.send(:create_representation_with, {}, {:exclude => [:groupies]}, Representable::Hash::PropertyBinding) assert_equal({"name"=>"No One's Choice"}, hash) end it "accepts :include option" do hash = @band.send(:create_representation_with, {}, {:include => [:groupies]}, Representable::Hash::PropertyBinding) assert_equal({"groupies"=>2}, hash) end it "ignores non-readable properties" do @band = Class.new(Band) { property :name; collection :founder_ids, :readable => false; attr_accessor :founder_ids }.new @band.name = "Iron Maiden" @band.founder_ids = [1,2,3] hash = @band.send(:create_representation_with, {}, {}, Representable::Hash::PropertyBinding) assert_equal({"name" => "Iron Maiden"}, hash) end it "does not write nil attributes" do @band.groupies = nil assert_equal({"name"=>"No One's Choice"}, @band.send(:create_representation_with, {}, {}, Representable::Hash::PropertyBinding)) end it "writes false attributes" do @band.groupies = false assert_equal({"name"=>"No One's Choice","groupies"=>false}, @band.send(:create_representation_with, {}, {}, Representable::Hash::PropertyBinding)) end describe "when :render_nil is true" do it "includes nil attribute" do mod = Module.new do include Representable::JSON property :name property :groupies, :render_nil => true end @band.extend(mod) # FIXME: use clean object. @band.groupies = nil hash = @band.send(:create_representation_with, {}, {}, Representable::Hash::PropertyBinding) assert_equal({"name"=>"No One's Choice", "groupies" => nil}, hash) end it "includes nil attribute without extending" do mod = Module.new do include Representable::JSON property :name property :groupies, :render_nil => true, :extend => BandRepresentation end @band.extend(mod) # FIXME: use clean object. @band.groupies = nil hash = @band.send(:create_representation_with, {}, {}, Representable::Hash::PropertyBinding) assert_equal({"name"=>"No One's Choice", "groupies" => nil}, hash) end end it "does not propagate private options to nested objects" do cover_rpr = Module.new do include Representable::Hash property :title property :original, :extend => self end # FIXME: we should test all representable-options (:include, :exclude, ?) Class.new(OpenStruct).new(:title => "Roxanne", :original => Class.new(OpenStruct).new(:title => "Roxanne (Don't Put On The Red Light)")).extend(cover_rpr). to_hash(:include => [:original]).must_equal({"original"=>{"title"=>"Roxanne (Don't Put On The Red Light)"}}) end end describe ":if" do before do @pop = Class.new(PopBand) { attr_accessor :fame } end it "respects property when condition true" do @pop.class_eval { property :fame, :if => lambda { true } } band = @pop.new band.update_properties_from({"fame"=>"oh yes"}, {}, Representable::Hash::PropertyBinding) assert_equal "oh yes", band.fame end it "ignores property when condition false" do @pop.class_eval { property :fame, :if => lambda { false } } band = @pop.new band.update_properties_from({"fame"=>"oh yes"}, {}, Representable::Hash::PropertyBinding) assert_equal nil, band.fame end it "ignores property when :exclude'ed even when condition is true" do @pop.class_eval { property :fame, :if => lambda { true } } band = @pop.new band.update_properties_from({"fame"=>"oh yes"}, {:exclude => [:fame]}, Representable::Hash::PropertyBinding) assert_equal nil, band.fame end it "executes block in instance context" do @pop.class_eval { property :fame, :if => lambda { groupies } } band = @pop.new band.groupies = true band.update_properties_from({"fame"=>"oh yes"}, {}, Representable::Hash::PropertyBinding) assert_equal "oh yes", band.fame end describe "propagating user options to the block" do representer! do property :name, :if => lambda { |opts| opts[:include_name] } end subject { OpenStruct.new(:name => "Outbound").extend(representer) } it "works without specifying options" do subject.to_hash.must_equal({}) end it "passes user options to block" do subject.to_hash(:include_name => true).must_equal({"name" => "Outbound"}) end end end describe ":getter and :setter" do representer! do property :name, :getter => lambda { |args| "#{args[:welcome]} #{name}" }, :setter => lambda { |val, args| self.name = "#{args[:welcome]} #{val}" } end subject { OpenStruct.new(:name => "Mony Mony").extend(representer) } it "uses :getter when rendering" do subject.to_hash(:welcome => "Hi").must_equal({"name" => "Hi Mony Mony"}) end it "uses :setter when parsing" do subject.from_hash({"name" => "Eyes Without A Face"}, :welcome => "Hello").name.must_equal "Hello Eyes Without A Face" end end describe ":reader and :writer" do representer! do property :name, :writer => lambda { |doc, args| doc["title"] = "#{args[:nr]}) #{name}" }, :reader => lambda { |doc, args| self.name = doc["title"].split(") ").last } end subject { OpenStruct.new(:name => "Disorder And Disarray").extend(representer) } it "uses :writer when rendering" do subject.to_hash(:nr => 14).must_equal({"title" => "14) Disorder And Disarray"}) end it "uses :reader when parsing" do subject.from_hash({"title" => "15) The Wars End"}).name.must_equal "The Wars End" end end describe ":extend and :class" do module UpcaseRepresenter def to_hash(*); upcase; end def from_hash(hsh, *args); self.class.new hsh.upcase; end # DISCUSS: from_hash must return self. end module DowncaseRepresenter def to_hash(*); downcase; end def from_hash(hsh, *args); hsh.downcase; end end class UpcaseString < String; end describe "lambda blocks" do representer! do property :name, :extend => lambda { |name| compute_representer(name) } end it "executes lambda in represented instance context" do Song.new("Carnage").instance_eval do def compute_representer(name) UpcaseRepresenter end self end.extend(representer).to_hash.must_equal({"name" => "CARNAGE"}) end end describe ":instance" do obj = String.new("Fate") mod = Module.new { def from_hash(*); self; end } representer! do property :name, :extend => mod, :instance => lambda { |name| obj } end it "uses object from :instance but still extends it" do song = Song.new.extend(representer).from_hash("name" => "Eric's Had A Bad Day") song.name.must_equal obj song.name.must_be_kind_of mod end end describe "property with :extend" do representer! do property :name, :extend => lambda { |name| name.is_a?(UpcaseString) ? UpcaseRepresenter : DowncaseRepresenter }, :class => String end it "uses lambda when rendering" do assert_equal({"name" => "you make me thick"}, Song.new("You Make Me Thick").extend(representer).to_hash ) assert_equal({"name" => "STEPSTRANGER"}, Song.new(UpcaseString.new "Stepstranger").extend(representer).to_hash ) end it "uses lambda when parsing" do Song.new.extend(representer).from_hash({"name" => "You Make Me Thick"}).name.must_equal "you make me thick" Song.new.extend(representer).from_hash({"name" => "Stepstranger"}).name.must_equal "stepstranger" # DISCUSS: we compare "".is_a?(UpcaseString) end describe "with :class lambda" do representer! do property :name, :extend => lambda { |name| name.is_a?(UpcaseString) ? UpcaseRepresenter : DowncaseRepresenter }, :class => lambda { |fragment| fragment == "Still Failing?" ? String : UpcaseString } end it "creates instance from :class lambda when parsing" do song = Song.new.extend(representer).from_hash({"name" => "Quitters Never Win"}) song.name.must_be_kind_of UpcaseString song.name.must_equal "QUITTERS NEVER WIN" song = Song.new.extend(representer).from_hash({"name" => "Still Failing?"}) song.name.must_be_kind_of String song.name.must_equal "still failing?" end describe "when :class lambda returns nil" do representer! do property :name, :extend => lambda { |name| Module.new { def from_hash(data, *args); data; end } }, :class => nil end it "skips creating new instance" do song = Song.new.extend(representer).from_hash({"name" => string = "Satellite"}) song.name.object_id.must_equal string.object_id end end end end describe "collection with :extend" do representer! do collection :songs, :extend => lambda { |name| name.is_a?(UpcaseString) ? UpcaseRepresenter : DowncaseRepresenter }, :class => String end it "uses lambda for each item when rendering" do Album.new([UpcaseString.new("Dean Martin"), "Charlie Still Smirks"]).extend(representer).to_hash.must_equal("songs"=>["DEAN MARTIN", "charlie still smirks"]) end it "uses lambda for each item when parsing" do album = Album.new.extend(representer).from_hash("songs"=>["DEAN MARTIN", "charlie still smirks"]) album.songs.must_equal ["dean martin", "charlie still smirks"] # DISCUSS: we compare "".is_a?(UpcaseString) end describe "with :class lambda" do representer! do collection :songs, :extend => lambda { |name| name.is_a?(UpcaseString) ? UpcaseRepresenter : DowncaseRepresenter }, :class => lambda { |fragment| fragment == "Still Failing?" ? String : UpcaseString } end it "creates instance from :class lambda for each item when parsing" do album = Album.new.extend(representer).from_hash("songs"=>["Still Failing?", "charlie still smirks"]) album.songs.must_equal ["still failing?", "CHARLIE STILL SMIRKS"] end end end describe ":binding" do representer! do class MyBinding < Representable::Binding def write(doc, *args) doc[:title] = @represented.title end end property :title, :binding => lambda { |*args| MyBinding.new(*args) } end it "uses the specified binding instance" do OpenStruct.new(:title => "Affliction").extend(representer).to_hash.must_equal({:title => "Affliction"}) end end end describe "Config" do subject { Representable::Config.new } PunkRock = Class.new describe "wrapping" do it "returns false per default" do assert_equal nil, subject.wrap_for("Punk") end it "infers a printable class name if set to true" do subject.wrap = true assert_equal "punk_rock", subject.wrap_for(PunkRock) end it "can be set explicitely" do subject.wrap = "Descendents" assert_equal "Descendents", subject.wrap_for(PunkRock) end end describe "clone" do it "clones all definitions" do subject << Object.new assert subject.first != subject.clone.first end end end end