neuroncheck 0.1.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.
@@ -0,0 +1,245 @@
1
+ require 'neuroncheck/error'
2
+ require 'neuroncheck/utils'
3
+ require 'neuroncheck/plugin'
4
+
5
+ module NeuronCheckSystem
6
+ # 期待する値に対応する、適切なマッチャを取得
7
+ def self.get_appropriate_matcher(expected, declared_caller_locations)
8
+ case expected
9
+ when DeclarationContext # 誤って「self」と記載した場合
10
+ raise DeclarationError, "`self` cannot be used in declaration - use `:self` instead"
11
+ when :self # self
12
+ SelfMatcher.new(declared_caller_locations) # 値がselfであるかどうかチェック
13
+
14
+ when String, Symbol, Integer
15
+ ValueEqualMatcher.new(expected, declared_caller_locations) # 値が等しいかどうかをチェック
16
+ when true, false, nil
17
+ ObjectIdenticalMathcer.new(expected, declared_caller_locations) # オブジェクトが同一かどうかをチェック
18
+ when Class, Module
19
+ KindOfMatcher.new(expected, declared_caller_locations) # 所属/継承しているかどうかをチェック
20
+ when Range
21
+ RangeMatcher.new(expected, declared_caller_locations) # 範囲チェック
22
+ when Regexp
23
+ RegexpMatcher.new(expected, declared_caller_locations) # 正規表現チェック
24
+ # when Encoding
25
+ # EncodingMatcher.new(expected, declared_caller_locations) # エンコーディングチェック
26
+
27
+ when Array
28
+ OrMatcher.new(expected, declared_caller_locations) # ORチェック
29
+
30
+ when Plugin::Keyword # プラグインによって登録されたキーワードの場合
31
+ KeywordPluginMatcher.new(expected, declared_caller_locations)
32
+
33
+ else
34
+ raise DeclarationError, "#{expected.class.name} cannot be usable for NeuronCheck check parameter\n value: #{expected.inspect}"
35
+ end
36
+
37
+ end
38
+
39
+ class MatcherBase
40
+ attr_accessor :declared_caller_locations
41
+
42
+ def initialize(expected, declared_caller_locations)
43
+ @expected = expected
44
+ @declared_caller_locations = declared_caller_locations
45
+ end
46
+
47
+ def match?(value, self_object)
48
+ raise NotImplementedError
49
+ end
50
+
51
+ def get_error_message(signature_decl, context_caption, value, phrase_after_but: 'was')
52
+ locs = Utils.backtrace_locations_to_captions(@declared_caller_locations)
53
+
54
+ ret = ""
55
+ ret.concat(<<MSG)
56
+ #{context_caption} must be #{expected_caption}, but #{phrase_after_but} #{Utils.truncate(value.inspect, 40)}
57
+ got: #{value.inspect}
58
+ MSG
59
+
60
+ if signature_decl and signature_decl.assigned_method then
61
+ ret.concat(<<MSG)
62
+ signature: #{signature_decl.signature_caption}
63
+ MSG
64
+ end
65
+
66
+ if locs.size >= 1 then
67
+ ret.concat(<<MSG)
68
+ declared at: #{locs.join("\n" + ' ' * 15)}
69
+
70
+ MSG
71
+ end
72
+
73
+ ret
74
+ end
75
+
76
+ def expected_caption
77
+ raise NotImplementedError
78
+ end
79
+
80
+ def expected_short_caption
81
+ @expected.inspect
82
+ end
83
+
84
+ def meta_info_as_json
85
+ re = {}
86
+ re['type'] = self.class.name.split('::')[-1]
87
+ re['expected'] = @expected
88
+
89
+ re
90
+ end
91
+ end
92
+
93
+ # 値が == で等しいことを判定する
94
+ class ValueEqualMatcher < MatcherBase
95
+ def match?(value, self_object)
96
+ @expected == value
97
+ end
98
+
99
+ def expected_caption
100
+ @expected.inspect
101
+ end
102
+ end
103
+
104
+ # オブジェクトとして同一であることを判定する
105
+ class ObjectIdenticalMathcer < MatcherBase
106
+ def match?(value, self_object)
107
+ @expected.equal?(value)
108
+ end
109
+
110
+ def expected_caption
111
+ @expected.inspect
112
+ end
113
+ end
114
+
115
+ # 値が指定されたClass / Moduleに所属していることを判定する (kind_of?判定)
116
+ class KindOfMatcher < MatcherBase
117
+ def match?(value, self_object)
118
+ value.kind_of?(@expected)
119
+ end
120
+
121
+ def expected_caption
122
+ @expected.name
123
+ end
124
+
125
+ def meta_info_as_json
126
+ super.update('expected' => @expected.name)
127
+ end
128
+ end
129
+
130
+
131
+ # 指定した範囲に含まれている値であることを判定する
132
+ class RangeMatcher < MatcherBase
133
+ def match?(value, self_object)
134
+ @expected.include?(value)
135
+ end
136
+
137
+ def expected_caption
138
+ "included in #{@expected.inspect}"
139
+ end
140
+ end
141
+
142
+ # 指定した正規表現にマッチする文字列であることを判定する
143
+ class RegexpMatcher < MatcherBase
144
+ def match?(value, self_object)
145
+ (value.kind_of?(String) and @expected =~ value)
146
+ end
147
+
148
+ def expected_caption
149
+ "String that matches with #{@expected.inspect}"
150
+ end
151
+ end
152
+
153
+ # # 指定したエンコーディングを持つ文字列であることを判定する
154
+ # class EncodingMatcher < MatcherBase
155
+ # def match?(value, self_object)
156
+ # (value.kind_of?(String) and Encoding.compatible?(value, @expected))
157
+ # end
158
+ #
159
+ # def expected_caption
160
+ # "String that is compatible #{@expected.name} encoding"
161
+ # end
162
+ #
163
+ # def expected_short_caption
164
+ # "String compatible to #{@expected.name}"
165
+ # end
166
+ #
167
+ # end
168
+
169
+ # OR条件。渡されたマッチャ複数のうち、どれか1つでも条件を満たせばOK
170
+ class OrMatcher < MatcherBase
171
+ def initialize(child_expecteds, declared_caller_locations)
172
+ @child_matchers = child_expecteds.map{|x| NeuronCheckSystem.get_appropriate_matcher(x, declared_caller_locations)}
173
+ @declared_caller_locations = declared_caller_locations
174
+ end
175
+
176
+ def match?(value, self_object)
177
+ # どれか1つにマッチすればOK
178
+ @child_matchers.any?{|x| x.match?(value, self_object)}
179
+ end
180
+
181
+ def expected_caption
182
+ captions = @child_matchers.map{|x| x.expected_caption}
183
+
184
+ Utils.string_join_using_or_conjunction(captions)
185
+ end
186
+ def expected_short_caption
187
+ '[' + @child_matchers.map{|x| x.expected_short_caption}.join(', ') + ']'
188
+ end
189
+
190
+ def meta_info_as_json
191
+ super.update('child_matchers' => @child_matchers.map{|x| x.meta_info_as_json})
192
+ end
193
+ end
194
+
195
+
196
+ # selfであるかどうかを判定 (通常はreturns用)
197
+ class SelfMatcher < MatcherBase
198
+ def initialize(declared_caller_locations)
199
+ @declared_caller_locations = declared_caller_locations
200
+ end
201
+
202
+ def match?(value, self_object)
203
+ self_object.equal?(value)
204
+ end
205
+
206
+ def expected_caption
207
+ "self"
208
+ end
209
+ def expected_short_caption
210
+ "self"
211
+ end
212
+ end
213
+
214
+ # プラグインで追加されたキーワード用
215
+ class KeywordPluginMatcher < MatcherBase
216
+ def initialize(keyword, declared_caller_locations)
217
+ @keyword = keyword
218
+ @declared_caller_locations = declared_caller_locations
219
+ end
220
+
221
+ def match?(value, self_object)
222
+ @keyword.api = Plugin::KeywordAPI.new(@declared_caller_locations, self_object)
223
+ @keyword.match?(value)
224
+ end
225
+
226
+ def expected_caption
227
+ @keyword.api = Plugin::KeywordAPI.new(@declared_caller_locations)
228
+ @keyword.expected_caption
229
+ end
230
+
231
+ def expected_short_caption
232
+ @keyword.api = Plugin::KeywordAPI.new(@declared_caller_locations)
233
+ @keyword.expected_short_caption
234
+ end
235
+
236
+ def meta_info_as_json
237
+ @keyword.api = Plugin::KeywordAPI.new(@declared_caller_locations)
238
+ super.update('keyword' => keyword_name, 'expected_caption' => expected_caption).tap{|x| x.delete('expected')}.update(@keyword.get_params_as_json)
239
+ end
240
+
241
+ def keyword_name
242
+ (@keyword.class).instance_variable_get(:@keyword_name).to_s
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,149 @@
1
+ module NeuronCheckSystem
2
+ # 追加されたキーワード全てが動的に定義されていくモジュール
3
+ module Keywords
4
+ end
5
+
6
+ module Plugin
7
+
8
+ # NeuronCheckへ登録したキーワード(Symbol)と、それに対応するKeywordクラスの対応を格納したマップ
9
+ KEYWORD_CLASSES = {}
10
+
11
+ # キーワードの追加
12
+ def self.add_keyword(name, &block)
13
+ # すでに予約済みのキーワードであればエラー
14
+ if KEYWORD_CLASSES[name] then
15
+ raise PluginError, "the `#{name}' keyword has been already reserved"
16
+ end
17
+
18
+ # キーワードを表すクラスを作成
19
+ keyword_class = Class.new(Keyword, &block)
20
+
21
+ # 必要なインスタンスメソッドが全て定義されているかどうかを確認
22
+ if not keyword_class.method_defined?(:on_call) or
23
+ not keyword_class.method_defined?(:match?) or
24
+ not keyword_class.method_defined?(:expected_caption) then
25
+ raise PluginError, "##{__callee__} requires 3 method definitions - `on_call', `match?' and `expected_caption'"
26
+ end
27
+
28
+ # キーワードを登録
29
+ keyword_class.instance_variable_set(:@keyword_name, name)
30
+ KEYWORD_CLASSES[name] = keyword_class
31
+
32
+ # キーワードメソッドを定義
33
+ __define_keyword_method_to_module(name, keyword_class)
34
+ end
35
+
36
+ # キーワードの別名を定義
37
+ def self.alias_keyword(name, original_keyword_name)
38
+ # すでに予約済みのキーワードであればエラー
39
+ if KEYWORD_CLASSES[name] then
40
+ raise PluginError, "the `#{name}' keyword has been already reserved"
41
+ end
42
+
43
+ # 元キーワードが、自分が追加したキーワードの中になければエラー
44
+ unless KEYWORD_CLASSES[original_keyword_name] then
45
+ raise PluginError, "the `#{original_keyword_name}' keyword hasn't been reserved yet"
46
+ end
47
+
48
+ # 継承して別名クラスを作成
49
+ keyword_class = Class.new(KEYWORD_CLASSES[original_keyword_name])
50
+ keyword_class.instance_variable_set(:@keyword_name, name)
51
+ KEYWORD_CLASSES[name] = keyword_class
52
+
53
+ # キーワードメソッドを定義
54
+ __define_keyword_method_to_module(name, keyword_class)
55
+ end
56
+
57
+ # キーワード用のメソッドをKeywordモジュールへ定義する
58
+ def self.__define_keyword_method_to_module(name, keyword_class)
59
+ Keywords.module_eval do
60
+ define_method(name) do |*params|
61
+ # キーワードを生成
62
+ kw = keyword_class.new
63
+
64
+ # そのキーワードのon_callメソッドを実行
65
+ kw.on_call(*params)
66
+
67
+ # キーワードを返す
68
+ kw
69
+ end
70
+ end
71
+ end
72
+
73
+ # キーワードの削除
74
+ def self.remove_keyword(name)
75
+ # 自分が追加したキーワードの中になければエラー
76
+ unless KEYWORD_CLASSES[name] then
77
+ raise PluginError, "the `#{name}' keyword hasn't been reserved yet"
78
+ end
79
+
80
+ # 組み込みキーワードを削除使用とした場合はエラー
81
+ if KEYWORD_CLASSES[name].builtin_keyword? then
82
+ raise PluginError, "the `#{name}' keyword cannot be removed because it is NeuronCheck builtin keyword"
83
+ end
84
+
85
+ # キーワードを表すクラスを削除
86
+ KEYWORD_CLASSES.delete(name)
87
+
88
+ # キーワードメソッドの定義を削除
89
+ Keywords.module_eval do
90
+ remove_method(name)
91
+ end
92
+ end
93
+
94
+ # キーワードクラス
95
+ class Keyword
96
+ attr_accessor :api
97
+
98
+ def expected_short_caption
99
+ expected_caption
100
+ end
101
+
102
+ def get_params_as_json
103
+ {}
104
+ end
105
+
106
+ def self.builtin_keyword?
107
+ false
108
+ end
109
+ end
110
+
111
+ # キーワードの処理内で使用可能なAPI
112
+ class KeywordAPI
113
+ def initialize(declared_caller_locations, method_self_object = nil)
114
+ @declared_caller_locations = declared_caller_locations
115
+ @method_self_object = method_self_object
116
+ end
117
+
118
+ def get_appropriate_matcher(expected_value)
119
+ NeuronCheckSystem.get_appropriate_matcher(expected_value, @declared_caller_locations)
120
+ end
121
+
122
+ def expected_value_match?(value, expected_value)
123
+ get_appropriate_matcher(expected_value).match?(value, @method_self_object)
124
+ end
125
+
126
+ def get_expected_value_caption(expected_value)
127
+ get_appropriate_matcher(expected_value).expected_caption
128
+
129
+ end
130
+
131
+ def get_expected_value_short_caption(expected_value)
132
+ get_appropriate_matcher(expected_value).expected_short_caption
133
+
134
+ end
135
+
136
+ def get_expected_value_meta_info_as_json(expected_value)
137
+ get_appropriate_matcher(expected_value).meta_info_as_json
138
+
139
+ end
140
+ end
141
+ end
142
+ end
143
+
144
+ module NeuronCheck
145
+ # プラグイン有効化
146
+ def self.enable_plugin(plugin_name)
147
+ require "neuroncheck/plugin/#{plugin_name}"
148
+ end
149
+ end
@@ -0,0 +1,49 @@
1
+ require 'neuroncheck/builtin_keyword'
2
+
3
+ # NeuronCheckSyntaxはruby 2.0以前では使用しない (Refinementが実験的機能として定義されているため)
4
+ unless RUBY_VERSION <= '2.0.9' then
5
+ NeuronCheckSystem::RUBY_TOPLEVEL = self
6
+
7
+ module NeuronCheckSyntax
8
+ refine Module do
9
+ # NeuronCheckの宣言用キーワードを、コード内の全箇所で使用可能にする
10
+ include NeuronCheckSystem::Keywords
11
+
12
+ # ndecl宣言 (このメソッドは初回実行時のみ呼び出されることに注意。1度ndeclを実行したら、次以降はNeuronCheckSystem::DeclarationMethodsの方が有効になるため、そちらが呼ばれる)
13
+ def ndecl(*expecteds, &block)
14
+ # モジュール/クラス内の場合の処理
15
+ # extend NeuronCheckが実行されていない未初期化の場合、NeuronCheck用の初期化を自動実行
16
+ unless @__neuron_check_extended then
17
+ extend NeuronCheck
18
+ end
19
+
20
+ # メイン処理実行
21
+ __neuroncheck_ndecl_main(expecteds, block, caller(1, 1))
22
+ end
23
+
24
+ alias ndeclare ndecl
25
+ alias ncheck ndecl
26
+ alias ntypesig ndecl
27
+ alias nsig ndecl
28
+
29
+ alias decl ndecl
30
+ alias declare ndecl
31
+ alias sig ndecl
32
+ end
33
+
34
+ # トップレベル定義のエイリアス
35
+ refine NeuronCheckSystem::RUBY_TOPLEVEL.singleton_class do
36
+ def decl(*expecteds, &block)
37
+ ndecl(*expecteds, &block)
38
+ end
39
+
40
+ def declare(*expecteds, &block)
41
+ ndecl(*expecteds, &block)
42
+ end
43
+
44
+ def sig(*expecteds, &block)
45
+ ndecl(*expecteds, &block)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,92 @@
1
+ module NeuronCheckSystem
2
+ module Utils
3
+ module_function
4
+
5
+ # From ActiveSupport (Thanks for Rails Team!) <https://github.com/rails/rails/tree/master/activesupport>
6
+ #
7
+ # Truncates a given +text+ after a given <tt>length</tt> if +text+ is longer than <tt>length</tt>:
8
+ #
9
+ # 'Once upon a time in a world far far away'.truncate(27)
10
+ # # => "Once upon a time in a wo..."
11
+ #
12
+ # Pass a string or regexp <tt>:separator</tt> to truncate +text+ at a natural break:
13
+ #
14
+ # 'Once upon a time in a world far far away'.truncate(27, separator: ' ')
15
+ # # => "Once upon a time in a..."
16
+ #
17
+ # 'Once upon a time in a world far far away'.truncate(27, separator: /\s/)
18
+ # # => "Once upon a time in a..."
19
+ #
20
+ # The last characters will be replaced with the <tt>:omission</tt> string (defaults to "...")
21
+ # for a total length not exceeding <tt>length</tt>:
22
+ #
23
+ # 'And they found that many people were sleeping better.'.truncate(25, omission: '... (continued)')
24
+ # # => "And they f... (continued)"
25
+ def truncate(str, truncate_at, omission: '...', separator: nil)
26
+ return str.dup unless str.length > truncate_at
27
+
28
+ omission = omission || '...'
29
+ length_with_room_for_omission = truncate_at - omission.length
30
+ stop = \
31
+ if separator
32
+ rindex(separator, length_with_room_for_omission) || length_with_room_for_omission
33
+ else
34
+ length_with_room_for_omission
35
+ end
36
+
37
+ "#{str[0, stop]}#{omission}"
38
+ end
39
+
40
+ # 1つ以上の文字列をorで結んだ英語文字列にする
41
+ def string_join_using_or_conjunction(strings)
42
+ ret = ""
43
+ strings.each_with_index do |str, i|
44
+ case i
45
+ when 0 # 最初の要素
46
+ when strings.size - 1 # 最後の要素
47
+ ret << " or "
48
+ else
49
+ ret << ", "
50
+ end
51
+
52
+ ret << str
53
+ end
54
+
55
+ ret
56
+ end
57
+
58
+ # Thread::Backtrace::Locationのリストを文字列形式に変換。フレーム数が多すぎる場合は途中を省略
59
+ def backtrace_locations_to_captions(locations)
60
+ locs = nil
61
+ if locations.size > 9 then
62
+ locs = (locations[0..3].map{|x| "from #{x.to_s}"} + [" ... (#{locations.size - 8} frames) ..."] + locations[-4..-1].map{|x| "from #{x.to_s}"})
63
+ else
64
+ locs = locations.map{|x| "from #{x.to_s}"}
65
+ end
66
+
67
+ if locs.size >= 1 then
68
+ locs.first.sub!(/\A\s*from /, '')
69
+ end
70
+
71
+ locs
72
+ end
73
+
74
+ # 指定した整数値を序数文字列にする
75
+ def ordinalize(v)
76
+ if [11,12,13].include?(v % 100)
77
+ "#{v}th"
78
+ else
79
+ case (v % 10)
80
+ when 1
81
+ "#{v}st"
82
+ when 2
83
+ "#{v}nd"
84
+ when 3
85
+ "#{v}rd"
86
+ else
87
+ "#{v}th"
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end