Skip to content

Instantly share code, notes, and snippets.

@waterlink
Created November 7, 2014 09:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save waterlink/997f2106aacef0e520fe to your computer and use it in GitHub Desktop.
Save waterlink/997f2106aacef0e520fe to your computer and use it in GitHub Desktop.
Struct implementation as a module to extend
require 'rspec'
module StructDsl
def with_attributes(*args)
include Enumerable
args.map!(&:to_sym)
args.freeze
args.each { |p| attr_accessor p }
define_method(:initialize) do |*values|
args.each_with_index { |p, i| send(:"#{p}=", values[i]) }
end
define_method(:[]) do |key|
raise ArgumentError unless members.include?(key.to_sym)
public_send(key)
end
define_method(:[]=) do |key, value|
raise ArgumentError unless members.include?(key.to_sym)
public_send(:"#{key}=", value)
end
define_method(:members) { args }
define_method(:each) do |&blk|
members.each(&blk)
end
define_method(:each_pair) do |&blk|
each.map { |e| [e, self[e]] }.each(&blk)
end
define_method(:select) do |&blk|
values.select(&blk)
end
define_method(:values) { each.map { |e| self[e] } }
define_method(:hash) do
to_h.hash
end
define_method(:==) do |other|
return false unless self.class === other
self.each_pair.to_a == other.each_pair.to_a
end
define_method(:inspect) do
"<struct #{self.class.name} #{each_pair.map { |k, v| "#{k}=#{v.inspect}" }.join(", ")}"
end
define_method(:to_s) { inspect }
define_method(:length) { members.length }
define_method(:size) { length }
define_method(:to_a) { values.to_a }
define_method(:to_h) { each_pair.to_h }
define_method(:values_at) { |*a| to_h.values_at(*a) }
class << self
define_method(:call) do |*values|
new(*values)
end
alias :[] :call
end
end
end
class HelloWorld
extend StructDsl
with_attributes :greeting, :name
end
RSpec.describe "HelloWorld" do
let(:hello_world) { HelloWorld["hello", "world"] }
let(:other_hello_world) { HelloWorld["hello", "world"] }
let(:other_greeting) { HelloWorld["hi", "alex"] }
it { expect(hello_world).to eq(other_hello_world) }
it { expect(hello_world).not_to eq(other_greeting) }
it { expect(hello_world).not_to eq(nil) }
it { expect(hello_world).not_to eq(Object.new) }
it { expect(hello_world.hash).to eq(other_hello_world.hash) }
it { expect(hello_world.hash).not_to eq(0) } # unlikely to be false
it { expect(hello_world[:greeting]).to eq("hello") }
it { expect(hello_world["greeting"]).to eq("hello") }
it { expect{ hello_world[:greeting] = "hallo" }.to change(hello_world, :greeting).to("hallo") }
it { expect{ hello_world["greeting"] = "hallo" }.to change(hello_world, :greeting).to("hallo") }
it { expect(hello_world).to be(hello_world) }
it { expect(hello_world).not_to be(other_hello_world) }
it { expect(hello_world.inspect).to eq(%{<struct HelloWorld greeting="hello", name="world"}) }
it { expect(hello_world.to_s).to eq(%{<struct HelloWorld greeting="hello", name="world"}) }
it { expect(hello_world.length).to eq(2) }
it { expect(hello_world.size).to eq(2) }
it { expect(hello_world.select { |v| v =~ /e/ }).to eq(["hello"]) }
it { expect(other_greeting.select { |v| v =~ /e/ }).to eq(["alex"]) }
it { expect(hello_world.to_a).to eq(["hello", "world"]) }
it { expect(hello_world.to_h).to eq({ greeting: "hello", name: "world" }) }
it { expect(hello_world.values).to eq(["hello", "world"]) }
it { expect(hello_world.values_at(:greeting)).to eq(["hello"]) }
it { expect(hello_world.values_at(:name)).to eq(["world"]) }
it { expect(hello_world.values_at(:name, :greeting)).to eq(["world", "hello"]) }
it { expect(hello_world.all? { |k| k.is_a?(Symbol) }).to eq(true) }
it { expect(hello_world.all? { |k| k.to_s =~ /t/ }).to eq(false) }
it "does not have anything weird in hierarchy" do
# PP::ObjectMixin is added to Object by RSpec.
expect(HelloWorld.ancestors).to eq([HelloWorld, Enumerable, Object, PP::ObjectMixin, Kernel, BasicObject])
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment