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 +4 -4
- data/.travis.yml +0 -3
- data/README.md +1 -1
- data/benchmark/Gemfile +5 -0
- data/benchmark/bench.rb +35 -0
- data/lib/teyu.rb +129 -46
- data/lib/teyu/version.rb +1 -1
- data/teyu.gemspec +1 -1
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 41d078479a7fd57cf1428b9722942ef7f841e39dfda2fbc3c77283b341a5e485
|
4
|
+
data.tar.gz: 467cd3079389e47959b0be66aa5527290a6d65ce547c6ebc6117801a28cf7860
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 831c336a26f241cf4ddfb45f9771142601fdb4d223346d090e276fc8d385c1be0f56060255f7fc6f8ed84eb091e5e0fbc2fd8efd97bff6c1874551ede716f7a6
|
7
|
+
data.tar.gz: bd08a81adc45840d85df4c673ffa4709785899c695fb9c4679ac7431c46264ba12c75a7262e1258c9772369dc31ba489a8132cb7243f25de072ed1310bb6d070
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
data/benchmark/Gemfile
ADDED
data/benchmark/bench.rb
ADDED
@@ -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(*
|
7
|
-
|
8
|
-
|
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
|
12
|
-
def initialize(klass,
|
15
|
+
class FastInitializer
|
16
|
+
def initialize(klass, argument)
|
13
17
|
@klass = klass
|
14
|
-
@
|
18
|
+
@argument = argument
|
19
|
+
end
|
20
|
+
|
21
|
+
def define
|
22
|
+
@klass.class_eval(def_initialize, __FILE__, __LINE__)
|
15
23
|
end
|
16
24
|
|
17
|
-
def
|
18
|
-
|
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
|
-
|
21
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
132
|
+
def initialize(params)
|
133
|
+
@params = params
|
134
|
+
validate
|
49
135
|
end
|
50
136
|
|
51
|
-
def
|
52
|
-
arg_names.
|
53
|
-
|
54
|
-
raise ArgumentError, "
|
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
|
-
|
63
|
-
|
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>]
|
68
|
-
def
|
69
|
-
@
|
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
|
156
|
+
# @return [Array<Symbol>] names of keyword arguments
|
74
157
|
def keyword_args
|
75
|
-
@keyword_args ||=
|
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>]
|
80
|
-
def
|
81
|
-
@
|
82
|
-
|
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
|
87
|
-
def
|
88
|
-
@
|
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
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", "
|
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.
|
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-
|
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
|