require 'test_helper' class RepresentableTest < MiniTest::Spec class Band include Representable::Hash 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 describe "in module" do 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 end end 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 it "allows mixing in multiple representers" do 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, :as => "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 # test if we call super in # ::inherited # ::included # ::extended module Representer include Representable # overrides ::inherited. end class BaseClass def self.inherited(subclass) super subclass.instance_eval { def other; end } end include Representable # overrides ::inherited. include Representer end class SubClass < BaseClass # triggers Representable::inherited, then OtherModule::inherited. end # test ::inherited. it do _(BaseClass.respond_to?(:other)).must_equal false _(SubClass.respond_to?(:other)).must_equal true end module DifferentIncluded def included(includer) includer.instance_eval { def different; end } end end module CombinedIncluded extend DifferentIncluded # defines ::included. include Representable # overrides ::included. end class IncludingClass include Representable include CombinedIncluded end # test ::included. it do IncludingClass.respond_to?(:representable_attrs) # from Representable IncludingClass.respond_to?(:different) end end describe "#property" do it "doesn't modify options hash" do options = {} representer.property(:title, options) _(options).must_equal({}) end representer! {} it "returns the Definition instance" do _(representer.property(:name)).must_be_kind_of Representable::Definition end end describe "#collection" do class RockBand < Band collection :albums end it "creates correct Definition" do assert_equal "albums", RockBand.representable_attrs.get(:albums).name assert RockBand.representable_attrs.get(:albums).array? end end describe "#hash" do it "also responds to the original method" do assert_kind_of Integer, BandRepresentation.hash end end class Hometown attr_accessor :name end module HometownRepresentable include Representable::JSON property :name 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 property :hometown, class: Hometown, extend: HometownRepresentable attr_accessor :name, :groupies, :hometown end describe "#update_properties_from" do before do @band = PopBand.new end it "copies values from document to object" do @band.from_hash({"name"=>"No One's Choice", "groupies"=>2}) assert_equal "No One's Choice", @band.name assert_equal 2, @band.groupies end it "ignores non-writeable properties" do @band = Class.new(Band) { property :name; collection :founders, :writeable => false; attr_accessor :founders }.new @band.from_hash("name" => "Iron Maiden", "groupies" => 2, "founders" => ["Steve Harris"]) assert_equal "Iron Maiden", @band.name assert_nil @band.founders end it "always returns the represented" do assert_equal @band, @band.from_hash({"name"=>"Nofx"}) end it "includes false attributes" do @band.from_hash({"groupies"=>false}) refute @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.from_hash({}) end # FIXME: do we need this test with XML _and_ JSON? it "ignores (no-default) properties not present in the incoming document" do { Representable::Hash => [:from_hash, {}], Representable::XML => [:from_xml, xml(%{}).to_s] }.each do |format, config| nested_repr = Module.new do # this module is never applied. # FIXME: can we make that a simpler test? 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.send(config.first, config.last) assert_nil @band.name, "Failed in #{format}" end end describe "passing options" do module TrackRepresenter include Representable::Hash end representer! do property :track, class: OpenStruct do property :nr property :length, class: OpenStruct do def to_hash(options) {seconds: options[:user_options][:nr]} end def from_hash(hash, options) super.tap do self.seconds = options[:user_options][:nr] end end end def to_hash(options) super.merge({"nr" => options[:user_options][:nr]}) end def from_hash(data, options) super.tap do self.nr = options[:user_options][:nr] end end end end it "#to_hash propagates to nested objects" do _(OpenStruct.new(track: OpenStruct.new(nr: 1, length: OpenStruct.new(seconds: nil))).extend(representer).extend(Representable::Debug). to_hash(user_options: {nr: 9})).must_equal({"track"=>{"nr"=>9, "length"=>{seconds: 9}}}) end it "#from_hash propagates to nested objects" do song = OpenStruct.new.extend(representer).from_hash({"track"=>{"nr" => "replace me", "length"=>{"seconds"=>"replacing"}}}, user_options: {nr: 9}) _(song.track.nr).must_equal 9 _(song.track.length.seconds).must_equal 9 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.to_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.to_hash 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.to_hash) end it "writes false attributes" do @band.groupies = false assert_equal({"name"=>"No One's Choice","groupies"=>false}, @band.to_hash) end end describe ":extend and :class" do module UpcaseRepresenter include Representable def to_hash(*); upcase; end def from_hash(hsh, *args); replace hsh.upcase; end # DISCUSS: from_hash must return self. end module DowncaseRepresenter include Representable def to_hash(*); downcase; end def from_hash(hsh, *args); replace 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 { include Representable; def from_hash(*); self; end } representer! do property :name, :extend => mod, :instance => lambda { |*| 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 { |options| options[:input].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 { |options| options[:input].is_a?(UpcaseString) ? UpcaseRepresenter : DowncaseRepresenter }, :class => lambda { |options| options[:fragment] == "Still Failing?" ? String : UpcaseString } end it "creates instance from :class lambda when parsing" do song = OpenStruct.new.extend(representer).from_hash({"name" => "Quitters Never Win"}) _(song.name).must_be_kind_of UpcaseString _(song.name).must_equal "QUITTERS NEVER WIN" song = OpenStruct.new.extend(representer).from_hash({"name" => "Still Failing?"}) _(song.name).must_be_kind_of String _(song.name).must_equal "still failing?" end end end describe "collection with :extend" do representer! do collection :songs, :extend => lambda { |options| options[:input].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 { |options| options[:input].is_a?(UpcaseString) ? UpcaseRepresenter : DowncaseRepresenter }, :class => lambda { |options| options[:input] == "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 ":decorator" do let(:extend_rpr) { Module.new { include Representable::Hash; collection :songs, :extend => SongRepresenter } } let(:decorator_rpr) { Module.new { include Representable::Hash; collection :songs, :decorator => SongRepresenter } } let(:songs) { [Song.new("Bloody Mary")] } it "is aliased to :extend" do _(Album.new(songs).extend(extend_rpr).to_hash).must_equal Album.new(songs).extend(decorator_rpr).to_hash end end # TODO: Move to global place since it's used twice. class SongRepresentation < Representable::Decorator include Representable::JSON property :name end class AlbumRepresentation < Representable::Decorator include Representable::JSON collection :songs, :class => Song, :extend => SongRepresentation end describe "::prepare" do let(:song) { Song.new("Still Friends In The End") } let(:album) { Album.new([song]) } describe "module including Representable" do it "uses :extend strategy" do album_rpr = Module.new { include Representable::Hash; collection :songs, :class => Song, :extend => SongRepresenter} _(album_rpr.prepare(album).to_hash).must_equal({"songs"=>[{"name"=>"Still Friends In The End"}]}) _(album).must_respond_to :to_hash end end describe "Decorator subclass" do it "uses :decorate strategy" do _(AlbumRepresentation.prepare(album).to_hash).must_equal({"songs"=>[{"name"=>"Still Friends In The End"}]}) _(album).wont_respond_to :to_hash end end end end end