teyu 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 87e7a03297458dbcd3b39f42889063698ab00c8e972b9161105073392af77d0a
4
- data.tar.gz: 3d399d485dba2dabb465f49c023bdeefc18b6cbf4390892240ff21c7ce1fd129
3
+ metadata.gz: 41d078479a7fd57cf1428b9722942ef7f841e39dfda2fbc3c77283b341a5e485
4
+ data.tar.gz: 467cd3079389e47959b0be66aa5527290a6d65ce547c6ebc6117801a28cf7860
5
5
  SHA512:
6
- metadata.gz: fd7163924f7aefd117dd6f724ed3d0da5b7bcd19af5ad085ca008a1442ef9f3127a829343fbad2a4ac8070d52e9d2304fda97ced1541b9eb19bc6cfb8e0b00e1
7
- data.tar.gz: 971732d8029ead2666a40a78a071568ceaaedd6325b4805b94c4497fd26c83603a68bc82989169e6b26d86025a1521c23b7e9d27e85c8755c2d25a65c1b99fa5
6
+ metadata.gz: 831c336a26f241cf4ddfb45f9771142601fdb4d223346d090e276fc8d385c1be0f56060255f7fc6f8ed84eb091e5e0fbc2fd8efd97bff6c1874551ede716f7a6
7
+ data.tar.gz: bd08a81adc45840d85df4c673ffa4709785899c695fb9c4679ac7431c46264ba12c75a7262e1258c9772369dc31ba489a8132cb7243f25de072ed1310bb6d070
data/.travis.yml CHANGED
@@ -3,9 +3,6 @@ language: ruby
3
3
  sudo: false
4
4
  cache: bundler
5
5
  rvm:
6
- - 2.3.0
7
- - 2.3.8
8
- - 2.4.5
9
6
  - 2.5.3
10
7
  - 2.6.3
11
8
  - ruby-head
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Teyu
1
+ # Teyu(手湯)
2
2
 
3
3
  A Ruby class extension for binding initialize method args to instance vars.
4
4
  Inspired by [attr_extras](https://github.com/barsoom/attr_extras) gem.
data/benchmark/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'benchmark_driver'
4
+ gem 'teyu', path: '../'
5
+ gem 'attr_extras'
@@ -0,0 +1,35 @@
1
+ require 'benchmark_driver'
2
+
3
+ Benchmark.driver do |x|
4
+ x.prelude <<~RUBY
5
+ require 'teyu'
6
+ require 'attr_extras'
7
+ class A
8
+ def initialize(a:, b:, c:, d:, e: 'e')
9
+ @a = a
10
+ @b = b
11
+ @c = c
12
+ @d = d
13
+ @e = e
14
+ end
15
+ end
16
+
17
+ class B
18
+ extend Teyu
19
+ teyu_init :a!, :b!, :c!, :d!, e: 'e'
20
+ end
21
+
22
+ class C
23
+ attr_initialize [:a, :b, :c, :d, e: 'e']
24
+ end
25
+
26
+ a, b, c, d = 'a', 'b', 'c', 'd'
27
+ A.new(a: a, b: b, c: c, d: d)
28
+ B.new(a: a, b: b, c: c, d: d)
29
+ C.new(a: a, b: b, c: c, d: d)
30
+ RUBY
31
+
32
+ x.report "Normal", %{ A.new(a: a, b: b, c: c, d: d) }
33
+ x.report "teyu", %{ B.new(a: a, b: b, c: c, d: d) }
34
+ x.report "attr_extras", %{ C.new(a: a, b: b, c: c, d: d) }
35
+ end
data/lib/teyu.rb CHANGED
@@ -3,89 +3,172 @@ require "teyu/version"
3
3
  module Teyu
4
4
  class Error < StandardError; end
5
5
 
6
- def teyu_init(*arg_names)
7
- define_initializer = DefineInitializer.new(self, arg_names)
8
- define_initializer.apply
6
+ def teyu_init(*params)
7
+ argument = Teyu::Argument.new(params)
8
+ begin
9
+ Teyu::FastInitializer.new(self, argument).define
10
+ rescue SyntaxError # fallback to slow, but generic initializer if failed
11
+ Teyu::GenericInitializer.new(self, argument).define
12
+ end
9
13
  end
10
14
 
11
- class DefineInitializer
12
- def initialize(klass, arg_names)
15
+ class FastInitializer
16
+ def initialize(klass, argument)
13
17
  @klass = klass
14
- @arg_names = arg_names
18
+ @argument = argument
19
+ end
20
+
21
+ def define
22
+ @klass.class_eval(def_initialize, __FILE__, __LINE__)
15
23
  end
16
24
 
17
- def apply
18
- sorter = Teyu::ArgsSorter.new(@arg_names)
25
+ private def def_initialize
26
+ <<~EOS
27
+ def initialize(#{def_initialize_args})
28
+ #{def_initialize_body}
29
+ end
30
+ EOS
31
+ end
19
32
 
20
- validate_number_of_req_args = method(:validate_number_of_req_args)
21
- validate_keyreq_args = method(:validate_keyreq_args)
33
+ private def def_initialize_args
34
+ args = []
35
+ args << "#{@argument.required_positional_args.map(&:to_s).join(', ')}"
36
+ args << "#{@argument.required_keyword_args.map { |arg| "#{arg}:" }.join(', ')}"
37
+ # LIMITATION:
38
+ # supports only default values which can be stringified such as `1`, `"a"`, `[1]`, `{a: 1}`.
39
+ # Note that the default value objects are newly created everytime on a method call.
40
+ args << "#{@argument.optional_keyword_args.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')}"
41
+ args.reject { |arg| arg.empty? }.join(', ')
42
+ end
22
43
 
23
- @klass.send(:define_method, :initialize) do |*arg_values|
24
- validate_number_of_req_args.call(sorter.req_args, arg_values)
25
- validate_keyreq_args.call(sorter.keyreq_args, arg_values)
44
+ private def def_initialize_body
45
+ @argument.arg_names.map { |name| "@#{name} = #{name}" }.join("\n ")
46
+ end
47
+ end
48
+
49
+ class GenericInitializer
50
+ def initialize(klass, argument)
51
+ @klass = klass
52
+ @argument = argument
53
+ end
26
54
 
27
- sorter.req_args.zip(arg_values).each do |name, value|
55
+ def define
56
+ # NOTE: accessing local vars is faster than method calls, so cache to local vars
57
+ required_positional_args = @argument.required_positional_args
58
+ required_keyword_args = @argument.required_keyword_args
59
+ optional_keyword_args = @argument.optional_keyword_args
60
+ keyword_args = @argument.keyword_args
61
+
62
+ @klass.define_method(:initialize) do |*given_args|
63
+ if given_args.last.is_a?(Hash)
64
+ given_positional_args = given_args[0...-1]
65
+ given_keyword_args = given_args.last
66
+ else
67
+ given_positional_args = given_args
68
+ given_keyword_args = {}
69
+ end
70
+ given_keyword_args_keys = given_keyword_args.keys
71
+
72
+ if required_positional_args.size != given_positional_args.size
73
+ raise ArgumentError, "wrong number of arguments (given #{given_positional_args.size}, expected #{required_positional_args.size})"
74
+ end
75
+ missing_keywords = required_keyword_args - given_keyword_args_keys
76
+ raise ArgumentError, "missing keywords: #{missing_keywords.join(', ')}" unless missing_keywords.empty?
77
+ unknown_keywords = given_keyword_args_keys - keyword_args
78
+ raise ArgumentError, "unknown keywords: #{unknown_keywords.join(', ')}" unless unknown_keywords.empty?
79
+
80
+ # NOTE: `while` is faster than `each` because it does not create a so-called "environment"
81
+ i = 0
82
+ while i < required_positional_args.size
83
+ name = required_positional_args[i]
84
+ value = given_positional_args[i]
28
85
  instance_variable_set(:"@#{name}", value)
86
+ i += 1
29
87
  end
30
88
 
31
- sorter.key_args.each do |name, value|
89
+ default_keyword_args_keys = optional_keyword_args.keys - given_keyword_args_keys
90
+ i = 0
91
+ while i < default_keyword_args_keys.size
92
+ name = default_keyword_args_keys[i]
93
+ value = optional_keyword_args[name]
94
+ # NOTE: In Ruby, objects of default arguments are newly created everytime on a method call.
95
+ #
96
+ # def test(a: "a")
97
+ # puts a.object_id
98
+ # end
99
+ # test #=> 70273097887660
100
+ # test #=> 70273097887860
101
+ #
102
+ # In a method argument, it is possible to suppress the new object creation like:
103
+ #
104
+ # $a = "a"
105
+ # def test(a: $a)
106
+ # puts a.object_id
107
+ # end
108
+ # test #=> 70273097887860
109
+ # test #=> 70273097887860
110
+ #
111
+ # But, we do not support a such feature in this gem. That's why we `dup` here.
112
+ value = value.dup
32
113
  instance_variable_set(:"@#{name}", value)
114
+ i += 1
33
115
  end
34
116
 
35
- keyword_arg_values = arg_values[sorter.req_args.length..].find { |value| value.is_a?(Hash) } || {}
36
- (sorter.keyword_args & keyword_arg_values.keys).each do |name|
37
- instance_variable_set(:"@#{name}", keyword_arg_values[name])
117
+ i = 0
118
+ while i < given_keyword_args_keys.size
119
+ name = given_keyword_args_keys[i]
120
+ value = given_keyword_args[name]
121
+ instance_variable_set(:"@#{name}", value)
122
+ i += 1
38
123
  end
39
124
  end
40
125
  end
126
+ end
41
127
 
42
- private
43
-
44
- def validate_number_of_req_args(arg_names, arg_values)
45
- req_arg_names_count = arg_names.count
46
- req_arg_values_count = arg_values.filter { |value| !value.is_a?(Hash) }.count
128
+ class Argument
129
+ REQUIRED_SYMBOL = '!'.freeze
130
+ VARIABLE_NAME_REGEXP = /\A[a-z_][a-z0-9_]*\z/
47
131
 
48
- raise ArgumentError, "wrong number of arguments (given #{req_arg_values_count}, expected #{req_arg_names_count})" if req_arg_names_count != req_arg_values_count
132
+ def initialize(params)
133
+ @params = params
134
+ validate
49
135
  end
50
136
 
51
- def validate_keyreq_args(arg_names, arg_values)
52
- arg_names.each do |name|
53
- keyword_arg_keys = arg_values.find { |value| value.is_a?(Hash) }&.keys || []
54
- raise ArgumentError, "(missing keyword: #{name})" unless keyword_arg_keys.include?(name)
137
+ private def validate
138
+ invalid_variable_names = arg_names.reject { |name| VARIABLE_NAME_REGEXP.match?(name) }
139
+ unless invalid_variable_names.empty?
140
+ raise ArgumentError, "invalid variable names: #{invalid_variable_names.join(', ')}"
55
141
  end
56
142
  end
57
- end
58
-
59
- class ArgsSorter
60
- REQUIRED_SYMBOL = '!'.freeze
61
143
 
62
- def initialize(names)
63
- @names = names
144
+ # @return [Array<Symbol>] names of arguments
145
+ def arg_names
146
+ @arg_names ||= required_positional_args + required_keyword_args + optional_keyword_args.keys
64
147
  end
65
148
 
66
149
  # method(a, b) => [:a, :b]
67
- # @return [Array<Symbol>] req arg names
68
- def req_args
69
- @req_args ||= @names.take_while { |arg| !arg.is_a?(Hash) && !arg.to_s.end_with?(REQUIRED_SYMBOL) }
150
+ # @return [Array<Symbol>] names of required positional arguments
151
+ def required_positional_args
152
+ @required_positional_args ||= @params.take_while { |arg| !arg.is_a?(Hash) && !arg.to_s.end_with?(REQUIRED_SYMBOL) }
70
153
  end
71
154
 
72
155
  # method(a!:, b: 'b') => [:a, :b]
73
- # @return [Array<Symbol>] keyword arg names
156
+ # @return [Array<Symbol>] names of keyword arguments
74
157
  def keyword_args
75
- @keyword_args ||= keyreq_args + key_args.keys
158
+ @keyword_args ||= required_keyword_args + optional_keyword_args.keys
76
159
  end
77
160
 
78
161
  # method(a!:, b!:) => [:a, :b]
79
- # @return [Array<Symbol>] keyreq arg names
80
- def keyreq_args
81
- @keyreq_args ||= @names.map(&:to_s).filter { |arg| arg.end_with?(REQUIRED_SYMBOL) }
82
- .map { |arg| arg.delete_suffix(REQUIRED_SYMBOL).to_sym }
162
+ # @return [Array<Symbol>] names of required keyword arguments
163
+ def required_keyword_args
164
+ @required_keyword_args ||= @params.map(&:to_s).select { |arg| arg.end_with?(REQUIRED_SYMBOL) }
165
+ .map { |arg| arg.delete_suffix(REQUIRED_SYMBOL).to_sym }
83
166
  end
84
167
 
85
168
  # method(a: 'a', b: 'b') => { a: 'a', b: 'b' }
86
- # @return [Hash] keyword args with default value
87
- def key_args
88
- @key_args ||= @names.filter { |arg| arg.is_a?(Hash) }&.inject(:merge) || {}
169
+ # @return [Hash] optional keyword arguments with their default values
170
+ def optional_keyword_args
171
+ @optional_keyword_args ||= @params.select { |arg| arg.is_a?(Hash) }&.inject(:merge) || {}
89
172
  end
90
173
  end
91
174
  end
data/lib/teyu/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Teyu
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
data/teyu.gemspec CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
20
20
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
21
  spec.require_paths = ["lib"]
22
22
 
23
- spec.add_development_dependency "bundler", "~> 1.17"
23
+ spec.add_development_dependency "bundler", ">= 1.17"
24
24
  spec.add_development_dependency 'rake'
25
25
  spec.add_development_dependency 'test-unit'
26
26
  spec.add_development_dependency 'power_assert'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: teyu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Takahiro Kiso (takanamito)
@@ -9,20 +9,20 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2019-08-07 00:00:00.000000000 Z
12
+ date: 2019-11-10 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  requirements:
18
- - - "~>"
18
+ - - ">="
19
19
  - !ruby/object:Gem::Version
20
20
  version: '1.17'
21
21
  type: :development
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
- - - "~>"
25
+ - - ">="
26
26
  - !ruby/object:Gem::Version
27
27
  version: '1.17'
28
28
  - !ruby/object:Gem::Dependency
@@ -81,6 +81,8 @@ files:
81
81
  - LICENSE.txt
82
82
  - README.md
83
83
  - Rakefile
84
+ - benchmark/Gemfile
85
+ - benchmark/bench.rb
84
86
  - bin/console
85
87
  - bin/setup
86
88
  - lib/teyu.rb