Skip to content

Add option to allow positional arguments #4

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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
31 changes: 16 additions & 15 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,31 @@ PATH
GEM
remote: https://rubygems.org/
specs:
diff-lcs (1.4.4)
rake (13.0.3)
rbs (1.5.1)
rspec (3.10.0)
rspec-core (~> 3.10.0)
rspec-expectations (~> 3.10.0)
rspec-mocks (~> 3.10.0)
rspec-core (3.10.1)
rspec-support (~> 3.10.0)
rspec-expectations (3.10.1)
diff-lcs (1.5.0)
rake (13.0.6)
rbs (1.8.1)
rspec (3.11.0)
rspec-core (~> 3.11.0)
rspec-expectations (~> 3.11.0)
rspec-mocks (~> 3.11.0)
rspec-core (3.11.0)
rspec-support (~> 3.11.0)
rspec-expectations (3.11.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.10.0)
rspec-mocks (3.10.2)
rspec-support (~> 3.11.0)
rspec-mocks (3.11.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.10.0)
rspec-support (3.10.2)
rspec-support (~> 3.11.0)
rspec-support (3.11.1)

PLATFORMS
ruby
x86_64-linux

DEPENDENCIES
rake (~> 13.0)
rspec (~> 3.0)
typed_struct!

BUNDLED WITH
2.1.4
2.4.0.dev
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,35 @@ clive.age = '22' # => Error
clive.preferences = { "opt_out_of_emails" => true, "additional" => nil } # error - type mismatch, not Symbol keys
clive.freeze # no more changes can be made
```
Optionally specify a default member value:
```ruby
3.2.0 :001 > TypedStruct.new({ default: 5 }, int: Integer, str: String)
=> #<Class:0x00007faeed58ab58>
3.2.0 :002 > _.new(str: "abc")
=> #<struct int=5, str="abc">
```
Pass [`Struct` options](https://ruby-doc.org/core-2.5.0/Struct.html#method-c-new) similarly:
```ruby
3.2.0 :001 > Struct.new("User", :name)
=> Struct::User
3.2.0 :002 > TypedStruct.new({ class_name: "User" }, name: String)
=> TypedStruct::User
3.2.0 :003 > Struct.new(:name, keyword_init: true)
=> #<Class:0x00007f86d618a1f0>(keyword_init: true)
3.2.0 :004 > TypedStruct.new({ keyword_init: true }, name: String)
=> #<Class:0x00007f86d618b190>(keyword_init: true)
```
Configure `TypedStruct.default_keyword_init` to change the default `keyword_init` value globally:
```ruby
3.2.0 :001 > TypedStruct.new(int: Integer, str: String)
=> #<Class:0x00007f6d32701f60>
3.2.0 :002 > TypedStruct.default_keyword_init = true
=> true
3.2.0 :003 > TypedStruct.new(int: Integer, str: String)
=> #<Class:0x00007f6d32706b00>(keyword_init: true)
3.2.0 :004 > TypedStruct.new({ keyword_init: false }, int: Integer, str: String)
=> #<Class:0x00007f6d32700fc0>
```

Note that a `TypedStruct` inherits from `Struct` directly, so anything from `Struct` is also available in `TypedStruct` - see [Struct docs](https://ruby-doc.org/core-3.0.1/Struct.html) for more info.

Expand Down
71 changes: 64 additions & 7 deletions lib/typed_struct.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,32 @@ class TypedStruct < Struct
alias_method :__class__, :class

class << self
@@default_keyword_init = nil

def default_keyword_init
@@default_keyword_init
end

def default_keyword_init=(default)
@@default_keyword_init = default
end

def new(opts = Options.new, **properties)
if opts[:keyword_init].nil?
opts[:keyword_init] = if RUBY_VERSION < "3.2"
default_keyword_init || false
else
default_keyword_init
end
end

properties.each_key do |prop|
if method_defined?(prop)
$stdout.puts OVERRIDING_NATIVE_METHOD_MSG % [prop.inspect, caller(3).first]
warn OVERRIDING_NATIVE_METHOD_MSG % [prop.inspect, caller(3).first]
end
end

super(*properties.keys, keyword_init: true).tap do |klass|
super(opts[:class_name], *properties.keys, keyword_init: opts[:keyword_init]).tap do |klass|
klass.class.instance_eval do
include TypeChecking
attr_reader :options
Expand All @@ -35,9 +53,25 @@ def new(opts = Options.new, **properties)
@options = { types: properties, options: opts }

define_method :[]= do |key, val|
if key.is_a?(Integer)
key = if key.negative?
offset = self.members.size + key
if offset.negative?
raise IndexError, "offset #{key} too small for struct(size:#{self.members.size})"
end
self.members[offset]
elsif key >= self.members.size
raise IndexError, "offset #{key} too large for struct(size:#{self.members.size})"
else
self.members[key]
end
end
unless properties.key?(key)
raise NameError, "no member '#{key}' in struct"
end
prop = properties[key]
unless val_is_type? val, prop
raise "Unexpected type #{val.class} for #{key.inspect} (expected #{prop})"
raise TypeError, "unexpected type #{val.class} for #{key.inspect} (expected #{prop})"
end

super key, val
Expand All @@ -53,18 +87,41 @@ def new(opts = Options.new, **properties)
end
end

def initialize(**attrs)
def initialize(*positional_attrs, **attrs)
opts = self.__class__.options
if opts[:options][:keyword_init] == true && !positional_attrs.empty?
raise ArgumentError, "wrong number of arguments (given #{positional_attrs.size}, expected 0)"
elsif (opts[:options][:keyword_init] == false && !attrs.empty?) ||
(opts[:options][:keyword_init] != true && !positional_attrs.empty?)
positional_attrs << attrs unless attrs.empty?
attrs = positional_attrs.zip(self.members).to_h(&:reverse)
end

if !positional_attrs.empty? && attrs.size > self.members.size
raise ArgumentError, "struct size differs"
elsif !(attrs.keys - self.members).empty?
raise ArgumentError, "unknown keywords: #{(attrs.keys - self.members).join(', ')}"
end

vals = opts[:types].to_h do |prop, expected_type|
value = attrs.fetch(prop, opts[:options][:default])
unless val_is_type? value, expected_type
raise "Unexpected type #{value.class} for #{prop.inspect} (expected #{expected_type})"
raise TypeError, "unexpected type #{value.class} for #{prop.inspect} (expected #{expected_type})"
end
[prop, value]
end

super **vals
if opts[:options][:keyword_init]
super **vals
else
super *vals.values
end
end

Options = TypedStruct.new({ default: nil }, default: Rbs("untyped"))
Options = TypedStruct.new(
{ default: nil, keyword_init: true },
default: Rbs("untyped"),
keyword_init: Rbs("bool?"),
class_name: Rbs("String? | Symbol?")
)
end
Loading