teyu 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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