Class: FlexMock::SignatureValidator

Inherits:
Object
  • Object
show all
Defined in:
lib/flexmock/validators.rb

Overview

Validate that the call matches a given signature

The validator created by #initialize matches any method call

Defined Under Namespace

Classes: ValidationFailed

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(expectation, required_arguments: 0, optional_arguments: 0, splat: true, required_keyword_arguments: [], optional_keyword_arguments: [], keyword_splat: true) ⇒ SignatureValidator

Returns a new instance of SignatureValidator.



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/flexmock/validators.rb', line 177

def initialize(
    expectation,
    required_arguments: 0,
    optional_arguments: 0,
    splat: true,
    required_keyword_arguments: [],
    optional_keyword_arguments: [],
    keyword_splat: true)
  @exp = expectation
  @required_arguments = required_arguments
  @optional_arguments = optional_arguments
  @required_keyword_arguments = required_keyword_arguments.to_set
  @optional_keyword_arguments = optional_keyword_arguments.to_set
  @splat = splat
  @keyword_splat = keyword_splat
end

Instance Attribute Details

#optional_argumentsObject (readonly)

The number of optional arguments



151
152
153
# File 'lib/flexmock/validators.rb', line 151

def optional_arguments
  @optional_arguments
end

#optional_keyword_argumentsSet<Symbol> (readonly)

The names of optional keyword arguments

Returns:



161
162
163
# File 'lib/flexmock/validators.rb', line 161

def optional_keyword_arguments
  @optional_keyword_arguments
end

#required_argumentsObject (readonly)

The number of required arguments



149
150
151
# File 'lib/flexmock/validators.rb', line 149

def required_arguments
  @required_arguments
end

#required_keyword_argumentsSet<Symbol> (readonly)

The names of required keyword arguments

Returns:



158
159
160
# File 'lib/flexmock/validators.rb', line 158

def required_keyword_arguments
  @required_keyword_arguments
end

Class Method Details

.from_instance_method(exp, instance_method) ⇒ Object

Create a validator that represents the signature of an existing method



291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/flexmock/validators.rb', line 291

def self.from_instance_method(exp, instance_method)
  required_arguments, optional_arguments, splat = 0, 0, false
  required_keyword_arguments, optional_keyword_arguments, keyword_splat = Set.new, Set.new, false
  instance_method.parameters.each do |type, name|
    case type
    when :req then required_arguments += 1
    when :opt then optional_arguments += 1
    when :rest then splat = true
    when :keyreq then required_keyword_arguments << name
    when :key then optional_keyword_arguments << name
    when :keyrest then keyword_splat = true
    when :block
    else raise ArgumentError, "cannot interpret parameter type #{type}"
    end
  end
  new(exp,
      required_arguments: required_arguments,
      optional_arguments: optional_arguments,
      splat: splat,
      required_keyword_arguments: required_keyword_arguments,
      optional_keyword_arguments: optional_keyword_arguments,
      keyword_splat: keyword_splat)
end

Instance Method Details

#describeObject



201
202
203
204
205
206
207
208
209
# File 'lib/flexmock/validators.rb', line 201

def describe
  ".with_signature(
      required_arguments: #{self.required_arguments},
      optional_arguments: #{self.optional_arguments},
      required_keyword_arguments: #{self.required_keyword_arguments.to_a},
      optional_keyword_arguments: #{self.optional_keyword_arguments.to_a},
      splat: #{self.splat?},
      keyword_splat: #{self.keyword_splat?})"
end

#expects_keyword_arguments?Boolean

Whether this method may have keyword arguments

Returns:

  • (Boolean)


168
169
170
# File 'lib/flexmock/validators.rb', line 168

def expects_keyword_arguments?
  keyword_splat? || !required_keyword_arguments.empty? || !optional_keyword_arguments.empty?
end

#keyword_splat?Boolean

Whether there is a splat for keyword arguments (double-star)

Returns:

  • (Boolean)


163
164
165
# File 'lib/flexmock/validators.rb', line 163

def keyword_splat?
  @keyword_splat
end

#null?Boolean

Whether this tests anything

It will return if this validator would validate any set of arguments

Returns:

  • (Boolean)


197
198
199
# File 'lib/flexmock/validators.rb', line 197

def null?
  splat? && keyword_splat?
end

#requires_keyword_arguments?Boolean

Whether this method may have keyword arguments

Returns:

  • (Boolean)


173
174
175
# File 'lib/flexmock/validators.rb', line 173

def requires_keyword_arguments?
  !required_keyword_arguments.empty?
end

#splat?Boolean

Whether there is a positional argument splat

Returns:

  • (Boolean)


153
154
155
# File 'lib/flexmock/validators.rb', line 153

def splat?
  @splat
end

#validate(args) ⇒ Object

Validates whether the given arguments match the expected signature

Parameters:

  • args (Array)

Raises:

  • ValidationFailed



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/flexmock/validators.rb', line 215

def validate(args)
  args = args.dup
  kw_args = Hash.new

  last_is_proc = false
  begin
    if args.last.kind_of?(Proc)
      args.pop
      last_is_proc = true
    end
  rescue NoMethodError
  end

  last_is_kw_hash = false
  if expects_keyword_arguments?
    last_is_kw_hash =
      begin
        args.last.kind_of?(Hash)
      rescue NoMethodError
      end

    if last_is_kw_hash
      kw_args = args.pop
    elsif requires_keyword_arguments?
      raise ValidationFailed, "#{@exp} expects keyword arguments but none were provided"
    end
  end

  # There is currently no way to disambiguate "given a block" from "given a
  # proc as last argument" ... give some leeway in this case
  positional_count = args.size

  if required_arguments > positional_count
    if requires_keyword_arguments?
      raise ValidationFailed, "#{@exp} expects at least #{required_arguments} positional arguments but got only #{positional_count}"
    end

    if (required_arguments - positional_count) == 1 && (last_is_kw_hash || last_is_proc)
      if last_is_kw_hash
        last_is_kw_hash = false
        kw_args = Hash.new
      else
        last_is_proc = false
      end
      positional_count += 1
    elsif (required_arguments - positional_count) == 2 && (last_is_kw_hash && last_is_proc)
      last_is_kw_hash = false
      kw_args = Hash.new
      last_is_proc = false
      positional_count += 2
    else
      raise ValidationFailed, "#{@exp} expects at least #{required_arguments} positional arguments but got only #{positional_count}"
    end
  end

  if !splat? && (required_arguments + optional_arguments) < positional_count
    if !last_is_proc || (required_arguments + optional_arguments) < positional_count - 1
      raise ValidationFailed, "#{@exp} expects at most #{required_arguments + optional_arguments} positional arguments but got #{positional_count}"
    end
  end

  missing_keyword_arguments = required_keyword_arguments.
    find_all { |k| !kw_args.has_key?(k) }
  if !missing_keyword_arguments.empty?
    raise ValidationFailed, "#{@exp} missing required keyword arguments #{missing_keyword_arguments.map(&:to_s).sort.join(", ")}"
  end
  if !keyword_splat?
    kw_args.each_key do |k|
      if !optional_keyword_arguments.include?(k) && !required_keyword_arguments.include?(k)
        raise ValidationFailed, "#{@exp} given unexpected keyword argument #{k}"
      end
    end
  end
end