Skip to content

Instantly share code, notes, and snippets.

@indigoviolet
Last active December 23, 2017 16:00
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 indigoviolet/1c7160a2ca99cf6eec824861cba4dc14 to your computer and use it in GitHub Desktop.
Save indigoviolet/1c7160a2ca99cf6eec824861cba4dc14 to your computer and use it in GitHub Desktop.
fin blog embed
# frozen_string_literal: true
module ValueStruct
include C
#
# ValueStruct is an immutable value object.
#
# Its constructor can take contracts. Define a class like:
# [2]> Foo = ValueStruct.define(bar: String, baz: C::Optional[C::Maybe[Hash]])
# => Foo
#
# Foo is now a class whose constructor must receive bar: String and baz: an
# optional nullable Hash
#
# [3]> ff = Foo.new(bar: 'string', baz: nil)
# => #<struct Foo bar="string", baz=nil>
#
# [4]> ff.bar
# => "string"
#
# It is immutable
# [5]> ff.bar = 'fad'
# ValueStruct::ImmutabilityError: ValueStruct::ImmutabilityError
#
# It can be cloned and tapped along the way:
# [6]> gg = ff.clone do |params|
# [6]* params[:bar] = 'fad'
# [6]* end
# => #<struct Foo bar="fad", baz=nil>
#
# [7]> gg.bar
# => "fad"
class ImmutabilityError < StandardError; end
# http://stackoverflow.com/questions/5407940/named-parameters-in-ruby-structs
class KeywordStruct < Struct
include C
def initialize(**kwargs)
super(*_get_struct_values(kwargs))
end
Contract Hash => ArrayOf[Any]
private def _get_struct_values(kwargs)
members.map { |k| kwargs[k] }
end
Contract Maybe[Func[Hash => Hash]] => KeywordStruct
public def clone
params = self.to_h
params = params.tap { |f| yield(f) } if block_given?
self.class.new(**params)
end
Contract HashOf[Symbol => Any], HashOf[Symbol => Any] => Any
private def _enforce_required_members!(data, member_contracts)
# (See #1 in https://github.com/egonSchiele/contracts.ruby/issues/282)
#
# The KeywordArgs contract appears to some deficiencies. A missing
# arg with a Maybe[] contract is valid despite not being declared
# optional:
#
# [9] development (main)> kk = Contracts::KeywordArgs[a: Contracts::Maybe[String]]
# [10] development (main)> kk.valid?({})
# => true
#
# In practice, this is often not an issue because the function definition would be like
#
# Contract Contracts::KeywordArgs[a: Contracts::Maybe[String]]
# def foo(a: )
# end
#
# and Ruby would enforce the presence of `a`.
#
# However, if you use
#
# Contract Contracts::KeywordArgs[a: Contracts::Maybe[String]]
# def foo(**kwargs)
# end
#
# this will break. This is the case here, unfortunately.
#
# To get around this until we can fix upstream, we explicitly look
# for Optional members and validate the presence of others
#
required_members = member_contracts.select { |_member, contract| !contract.is_a?(Contracts::Optional) }.keys
required_members.each do |member|
if !data.key?(member)
raise ArgumentError.new("Missing non-Optional member: #{member} in ValueStruct #{self.class.name}")
end
end
end
end
# Dynamically define a KeywordStruct with a contract on initialize()
# and disabled setters. Takes Symbol => Contract (which we can't
# represent in a contract) and returns a KeywordStruct
Contract Hash[Symbol => Any] => DescendantOf[KeywordStruct]
public_class_method def self.define(**args)
raise ArgumentError.new(
'This does not take a block, but you can inherit from it (e.g class Foo < ValueStruct.define(...))'
) if block_given?
KeywordStruct.new(*args.keys) do
include C
# This is a class instance variable! We allow instances to
# access it through the member_contracts class_method. This
# peculiar arrangement is necessary because `def` is a scope
# barrier and cannot "see" args.
@member_contracts = args
public_class_method def self.member_contracts
@member_contracts
end
Contract KeywordArgs[**args] => Any
def initialize(**params)
_enforce_required_members!(params, self.class.member_contracts)
super
end
args.keys.each do |k|
define_method "#{k}=" do |_val|
raise ImmutabilityError.new
end
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment