Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Representable::Object#to_object #266

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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::Object#to_object` (provides a Struct-based object)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then it should be to_struct.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we make the object type configurable and Struct is one of the possible targets?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this. But having a to_struct shortcut is good too.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, module Struct and to_struct fit more, as to_object is too vague. Moved the new code to a new module.

Frankly, I'm kinda confused about configurable object type with Struct as one of the possible targets. What other possible target there could be..? OpenStruct / AnyCustomUserClassPassedAsArgument? I feel like Struct is sufficient and there's no need for other options

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object can return the original object such as ActiveRecord model or a Poro with it attributes set.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Frankly, I'm kinda confused about configurable object type with Struct as one of the possible targets. What other possible target there could be..?

Imagine a project with Domain::Song, Domain::Song::Duration, Domain::Artist, etc. People might want to transform an object to another domain 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
1 change: 0 additions & 1 deletion lib/representable/hash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ def from_hash(data, options={}, binding_builder=Binding)

def to_hash(options={}, binding_builder=Binding)
hash = create_representation_with({}, options, binding_builder)

return hash if options[:wrap] == false
return hash unless (wrap = options[:wrap] || representation_wrap(options))

Expand Down
27 changes: 26 additions & 1 deletion lib/representable/object.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

module Representable
module Object
seuros marked this conversation as resolved.
Show resolved Hide resolved
autoload :Collection, 'representable/object/collection'

def self.included(base)
base.class_eval do
include Representable
Expand All @@ -13,17 +15,40 @@ def self.included(base)


module ClassMethods
def format_engine
Representable::Object
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 from_object(data, options={}, binding_builder=Binding)
update_properties_from(data, options, binding_builder)
end

def to_object(options={}, binding_builder=Binding)
create_representation_with(nil, options, binding_builder)
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
8 changes: 4 additions & 4 deletions lib/representable/object/binding.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ def self.build_for(definition) # TODO: remove default arg.
new(definition)
end

def read(hash, as)
fragment = hash.send(as) # :getter? no, that's for parsing!
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(hash, fragment, as)
true
def write(struct, fragment, as)
struct.send("#{as}=", fragment)
end

def deserialize_method
Expand Down
46 changes: 46 additions & 0 deletions lib/representable/object/collection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
require 'representable/hash'

module Representable::Object
module Collection
include Representable::Object

def self.included(base)
base.class_eval do
include Representable::Object
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
31 changes: 29 additions & 2 deletions test/examples/object.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

source = OpenStruct.new(
name: "30 Years Live", songs: [
OpenStruct.new(id: 1, title: "Dear Beloved"), OpenStruct.new(id: 2, title: "Fuck Armageddon")
]
OpenStruct.new(id: 1, title: "Dear Beloved"), OpenStruct.new(id: 2, title: "Fuck Armageddon")
]
)

require "representable/object"
Expand All @@ -26,3 +26,30 @@ class AlbumRepresenter < Representable::Decorator
target = Album.new

AlbumRepresenter.new(target).from_object(source)

# Representing to_object:

class Animal
attr_accessor :name, :age, :species
def initialize(name, age, species)
@name = name
@age = age
@species = species
end
end

require "representable/object"

class AnimalRepresenter < Representable::Decorator
include Representable::Object

property :name, getter: ->(represented:, **) { represented.name.upcase }
property :age
end

animal = Animal.new('Smokey',12,'c')
animal_repr = AnimalRepresenter.new(animal).to_object(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_object

86 changes: 82 additions & 4 deletions test/object_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,88 @@ class ObjectTest < MiniTest::Spec
end
end

it do
representer.prepare(source).to_object
_(source.album.name).must_equal "Live"
_(source.album.songs[0].title).must_equal 1
# it do
# representer.prepare(source).to_object
# _(source.album.name).must_equal "Live"
# _(source.album.songs[0].title).must_equal 1
# end
end
end

class ObjectPublicMethodsTest < Minitest::Spec
Song = Struct.new(:title, :album)
Album = Struct.new(:id, :name, :songs, :free_concert_ticket_promo_code)
class AlbumRepresenter < Representable::Decorator
include Representable::Object
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_object
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
seuros marked this conversation as resolved.
Show resolved Hide resolved
represented = AlbumRepresenter.new(album).to_object(cover_png: cover_png)
_(represented.id).must_equal album.id
_(represented.name).wont_equal album.name
_(represented.name).must_equal album.name.lstrip.strip
_(represented.songs[0].title).wont_equal album.songs[0].title
_(represented.songs[0].title).must_equal album.songs[0].title.upcase

_(album.respond_to?(:free_concert_ticket_promo_code)).must_equal true
_(represented.respond_to?(:free_concert_ticket_promo_code)).must_equal false

_(represented.cover_png).must_equal cover_png
end

it do
represented = AlbumRepresenter.new(album).to_object(cover_png: cover_png)
_(represented.id).must_equal album.id
_(represented.name).wont_equal album.name
_(represented.name).must_equal album.name.lstrip.strip
_(represented.songs[0].title).wont_equal album.songs[0].title
_(represented.songs[0].title).must_equal album.songs[0].title.upcase

_(album.respond_to?(:free_concert_ticket_promo_code)).must_equal true
_(represented.respond_to?(:free_concert_ticket_promo_code)).must_equal false

_(represented.cover_png).must_equal 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_object(cover_png: cover_png)
_(represented.size).must_equal albums.size
_(albums[0].respond_to?(:free_concert_ticket_promo_code)).must_equal true
_(represented[0].respond_to?(:free_concert_ticket_promo_code)).must_equal false
_(represented[0].cover_png).must_equal cover_png
_(represented[0].class.object_id).must_equal represented[1].class.object_id
end

let(:wrapper) { "cool_album" }
let(:second_wrapper) { "magnificent_album" }
it do
represented_array = AlbumRepresenter.for_collection.new(albums).to_object(wrap: wrapper)
represented_object = AlbumRepresenter.new(album).to_object(wrap: second_wrapper)

_(represented_array.respond_to?(wrapper)).must_equal true

_(represented_array.send(wrapper)[0].respond_to?(wrapper)).must_equal true
_(represented_array.send(wrapper)[0].send(wrapper).songs[0].title).must_equal albums[0].songs[0].title.upcase

_(represented_array.send(wrapper)[0].class.object_id).must_equal represented_array.send(wrapper)[1].class.object_id # wrapper struct class is the same for collection
_(represented_array.send(wrapper)[0].class.object_id).wont_equal represented_object.class.object_id # wrapper structs classes are different for different wrappers
end
end