diff --git a/.gitignore b/.gitignore index d51937d8..a4e77a54 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ Gemfile.lock *.swp *.swo bin +.idea diff --git a/README.md b/README.md index ddc35315..d1f7a483 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,18 @@ class AlbumRepresenter < Representable::Decorator end ``` +## Formats + +The gem supports JSON, XML, YAML, hashes and Struct-based objects for the representing output. +To use a specific format, include the corresponding module in your representer and use `to_` method on a represented object: + +- `Representable::JSON#to_json` +- `Representable::JSON#to_hash` (provides a hash instead of string) +- `Representable::Hash#to_hash` +- `Representable::Struct#to_struct` (provides a Struct-based object) +- `Representable::XML#to_xml` +- `Representable::YAML#to_yaml` + ## More Representable has many more features and can literally parse and render any kind of document to an arbitrary Ruby object graph. diff --git a/lib/representable/struct.rb b/lib/representable/struct.rb new file mode 100644 index 00000000..fb85003c --- /dev/null +++ b/lib/representable/struct.rb @@ -0,0 +1,49 @@ +require 'representable' +require 'representable/struct/binding' + +module Representable + module Struct + autoload :Collection, 'representable/struct/collection' + + def self.included(base) + base.class_eval do + include Representable + extend ClassMethods + register_feature Representable::Struct + end + end + + + module ClassMethods + def format_engine + Representable::Struct + end + + def collection_representer_class + Collection + end + + def cache_struct + @represented_struct ||= ::Struct.new(*representable_attrs.keys.map(&:to_sym)) + end + + def cache_wrapper_struct(wrap:) + struct_name = :"@_wrapper_struct_#{wrap}" + return instance_variable_get(struct_name) if instance_variable_defined?(struct_name) + + instance_variable_set(struct_name, ::Struct.new(wrap)) + end + end + + def to_struct(options={}, binding_builder=Binding) + represented_struct = self.class.cache_struct + + object = create_representation_with(represented_struct.new, options, binding_builder) + return object if options[:wrap] == false + return object unless (wrap = options[:wrap] || representation_wrap(options)) + + wrapper_struct = self.class.cache_wrapper_struct(wrap: wrap.to_sym) + wrapper_struct.new(object) + end + end +end diff --git a/lib/representable/struct/binding.rb b/lib/representable/struct/binding.rb new file mode 100644 index 00000000..be8d2992 --- /dev/null +++ b/lib/representable/struct/binding.rb @@ -0,0 +1,33 @@ +require 'representable/binding' + +module Representable + module Struct + class Binding < Representable::Binding + def self.build_for(definition) + return Collection.new(definition) if definition.array? + + new(definition) + end + + def read(struct, as) + fragment = struct.send(as) # :getter? no, that's for parsing! + + return FragmentNotFound if fragment.nil? and typed? + + fragment + end + + def write(struct, fragment, as) + struct.send("#{as}=", fragment) + end + + def serialize_method + :to_struct + end + + class Collection < self + include Representable::Binding::Collection + end + end + end +end diff --git a/lib/representable/struct/collection.rb b/lib/representable/struct/collection.rb new file mode 100644 index 00000000..ad2bd80e --- /dev/null +++ b/lib/representable/struct/collection.rb @@ -0,0 +1,46 @@ +require 'representable/hash' + +module Representable::Struct + module Collection + include Representable::Struct + + def self.included(base) + base.class_eval do + include Representable::Struct + extend ClassMethods + property(:_self, {:collection => true}) + end + end + + + module ClassMethods + def items(options={}, &block) + collection(:_self, options.merge(:getter => lambda { |*| self }), &block) + end + end + + # TODO: revise lonely collection and build separate pipeline where we just use Serialize, etc. + + def create_representation_with(doc, options, format) + options = normalize_options(**options) + options[:_self] = options + + bin = representable_bindings_for(format, options).first + + Collect[*bin.default_render_fragment_functions]. + (represented, {doc: doc, fragment: represented, options: options, binding: bin, represented: represented}) + end + + def update_properties_from(doc, options, format) + options = normalize_options(**options) + options[:_self] = options + + bin = representable_bindings_for(format, options).first + + value = Collect[*bin.default_parse_fragment_functions]. + (doc, fragment: doc, document: doc, options: options, binding: bin, represented: represented) + + represented.replace(value) + end + end +end diff --git a/test/examples/struct.rb b/test/examples/struct.rb new file mode 100644 index 00000000..95fbf5b5 --- /dev/null +++ b/test/examples/struct.rb @@ -0,0 +1,27 @@ +require "representable/struct" + +# Representing to_struct: +class Animal + attr_accessor :name, :age, :species + def initialize(name, age, species) + @name = name + @age = age + @species = species + end +end + +class AnimalRepresenter < Representable::Decorator + include Representable::Struct + + property :name, getter: ->(represented:, **) { represented.name.upcase } + property :age +end + +animal = Animal.new('Smokey',12,'c') +animal_repr = AnimalRepresenter.new(animal).to_struct(wrap: "wrapper") + +animal_array = [Animal.new('Shepard',22,'s'),Animal.new('Pickle',12,'c'),Animal.new('Rodgers',55,'e')] +array_repr = AnimalRepresenter.for_collection.new(animal_array).to_struct + +animal_klass = Struct.new("Animal", :name, :age, :species) +AnimalRepresenter.new(animal_klass.new("qwik", 12, "old")).to_struct(wrap: "wrapper") diff --git a/test/object_test.rb b/test/object_test.rb index d7908427..6c5cdc81 100644 --- a/test/object_test.rb +++ b/test/object_test.rb @@ -24,17 +24,17 @@ class ObjectTest < MiniTest::Spec it do representer.prepare(target).from_object(source) - _(target.title).must_equal "The King Is Dead" - _(target.album.name).must_equal "RUINER" - _(target.album.songs[0].title).must_equal "IN VINO VERITAS II" + assert_equal "The King Is Dead", target.title + assert_equal "RUINER", target.album.name + assert_equal "IN VINO VERITAS II", target.album.songs[0].title end # ignore nested object when nil it do representer.prepare(Song.new("The King Is Dead")).from_object(Song.new) - _(target.title).must_be_nil # scalar property gets overridden when nil. - _(target.album).must_be_nil # nested property stays nil. + assert_nil target.title # scalar property gets overridden when nil. + assert_nil target.album # nested property stays nil. end # to_object diff --git a/test/struct_test.rb b/test/struct_test.rb new file mode 100644 index 00000000..a1d4a3dc --- /dev/null +++ b/test/struct_test.rb @@ -0,0 +1,82 @@ +require "test_helper" +require "representable/struct" + +class StructPublicMethodsTest < Minitest::Spec + Song = Struct.new(:title, :album) + Album = Struct.new(:id, :name, :songs, :free_concert_ticket_promo_code) + class AlbumRepresenter < Representable::Decorator + include Representable::Struct + property :id + property :name, getter: ->(*) { name.lstrip.strip } + property :cover_png, getter: ->(options:, **) { options[:cover_png] } + collection :songs do + property :title, getter: ->(*) { title.upcase } + property :album, getter: ->(*) { album.upcase } + end + end + + #--- + # to_struct + let(:album) { Album.new(1, " Rancid ", [Song.new("In Vino Veritas II", "Rancid"), Song.new("The King Is Dead", "Rancid")], "S3KR3TK0D3") } + let(:cover_png) { "example.com/cover.png" } + it do + represented = AlbumRepresenter.new(album).to_struct(cover_png: cover_png) + assert_equal album.id, represented.id + refute_equal album.name, represented.name + assert_equal album.name.lstrip.strip, represented.name + refute_equal album.songs[0].title, represented.songs[0].title + assert_equal album.songs[0].title.upcase, represented.songs[0].title + + assert_respond_to album, :free_concert_ticket_promo_code + refute_respond_to represented, :free_concert_ticket_promo_code + + assert_equal cover_png, represented.cover_png + end + + it do + represented = AlbumRepresenter.new(album).to_struct(cover_png: cover_png) + assert_equal album.id, represented.id + refute_equal album.name, represented.name + assert_equal album.name.lstrip.strip, represented.name + refute_equal album.songs[0].title, represented.songs[0].title + assert_equal album.songs[0].title.upcase, represented.songs[0].title + + assert_respond_to album, :free_concert_ticket_promo_code + refute_respond_to represented, :free_concert_ticket_promo_code + + assert_equal cover_png, represented.cover_png + end + + let(:albums) do [ + Album.new(1, "Rancid", [Song.new("In Vino Veritas II", "Rancid"), Song.new("The King Is Dead", "Rancid")], "S3KR3TK0D3"), + Album.new(2, "Punk powerhouse", [Song.new("Hard Outside The Box", "Punk powerhous"), Song.new("Wonderful Noise", "Punk powerhous")], "S3KR3TK0D3"), + Album.new(3, "Into the Beyond", [Song.new("Rhythm of the night", "Into the Beyond"), Song.new("I'm blue", "Into the Beyond")], "S3KR3TK0D3"), + ] + end + + it do + represented = AlbumRepresenter.for_collection.new(albums).to_struct(cover_png: cover_png) + assert_equal albums.size, represented.size + assert_respond_to albums[0], :free_concert_ticket_promo_code + refute_respond_to represented[0], :free_concert_ticket_promo_code + assert_equal cover_png, represented[0].cover_png + assert_equal represented[1].class.object_id, represented[0].class.object_id + end + + let(:wrapper) { "cool_album" } + let(:second_wrapper) { "magnificent_album" } + it do + represented_array = AlbumRepresenter.for_collection.new(albums).to_struct(wrap: wrapper) + represented_object = AlbumRepresenter.new(album).to_struct(wrap: second_wrapper) + + assert_respond_to represented_array, wrapper + + assert_respond_to represented_array.send(wrapper)[0], wrapper + first_song_title_represented = represented_array.send(wrapper)[0].send(wrapper).songs[0].title + first_song_title_original = albums[0].songs[0].title + assert_equal first_song_title_original.upcase, first_song_title_represented + + assert_equal represented_array.send(wrapper)[0].class.object_id, represented_array.send(wrapper)[1].class.object_id # wrapper struct class is the same for collection + refute_equal represented_array.send(wrapper)[0].class.object_id, represented_object.class.object_id # wrapper structs classes are different for different wrappers + end +end