Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ Gemfile.lock
*.swp
*.swo
bin
.idea
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_<format>` 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.
Expand Down
49 changes: 49 additions & 0 deletions lib/representable/struct.rb
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions lib/representable/struct/binding.rb
Original file line number Diff line number Diff line change
@@ -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
46 changes: 46 additions & 0 deletions lib/representable/struct/collection.rb
Original file line number Diff line number Diff line change
@@ -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
27 changes: 27 additions & 0 deletions test/examples/struct.rb
Original file line number Diff line number Diff line change
@@ -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")
10 changes: 5 additions & 5 deletions test/object_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
82 changes: 82 additions & 0 deletions test/struct_test.rb
Original file line number Diff line number Diff line change
@@ -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