Module: Sig

Defined in:
lib/sig.rb,
lib/sig/version.rb

Defined Under Namespace

Classes: ArgumentTypeError, ResultTypeError

Constant Summary collapse

VERSION =
"1.0.1".freeze

Class Method Summary collapse

Class Method Details

.check_arguments(expected_arguments, arguments) ⇒ Object



62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/sig.rb', line 62

def self.check_arguments(expected_arguments, arguments)
  errors = ""

  arguments.each_with_index{ |argument, index|
    if error = valid_or_formatted_error(expected_arguments[index], argument)
      errors << error
    end
  }

  unless errors.empty?
    raise ArgumentTypeError, errors
  end
end

.check_arguments_with_keywords(expected_arguments, arguments, expected_keyword_arguments, keyword_arguments) ⇒ Object



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/sig.rb', line 76

def self.check_arguments_with_keywords(expected_arguments, arguments,
                                       expected_keyword_arguments, keyword_arguments)
  errors = ""

  arguments.each_with_index{ |argument, index|
    if error = valid_or_formatted_error(expected_arguments[index], argument)
      errors << error
    end
  }

  if expected_keyword_arguments
    keyword_arguments.each{ |key, keyword_argument|
      if error = valid_or_formatted_error(expected_keyword_arguments[key], keyword_argument)
        errors << error
      end
    }
  elsif error = valid_or_formatted_error(expected_arguments[arguments.size], keyword_arguments)
    errors << error
  end

  unless errors.empty?
    raise ArgumentTypeError, errors
  end
end

.check_result(expected_result, result) ⇒ Object



101
102
103
104
105
# File 'lib/sig.rb', line 101

def self.check_result(expected_result, result)
  unless matches? expected_result, result
    raise ResultTypeError, format_error(expected_result, result)
  end
end

.define(object, expected_arguments, expected_result = nil, method_name) ⇒ Object



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/sig.rb', line 11

def self.define(object, expected_arguments, expected_result = nil, method_name)
  expected_arguments = Array(expected_arguments)
  if expected_arguments.last.is_a?(Hash)
    expected_keyword_arguments = expected_arguments.delete_at(-1)
  else
    expected_keyword_arguments = nil
  end

  method_visibility = get_method_visibility_or_raise(object, method_name)
  signature_checker = get_or_create_signature_checker(object)
  signature_checker.send :define_method, method_name do |*arguments, **keyword_arguments|
    if keyword_arguments.empty?
      ::Sig.check_arguments(expected_arguments, arguments)
      result = super(*arguments)
    else
      ::Sig.check_arguments_with_keywords(expected_arguments, arguments,
                                          expected_keyword_arguments, keyword_arguments)
      result = super(*arguments, **keyword_arguments)
    end
    ::Sig.check_result(expected_result, result)

    result
  end
  signature_checker.send(method_visibility, method_name)

  method_name
end

.format_error(expected, value) ⇒ Object



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/sig.rb', line 138

def self.format_error(expected, value)
  case expected
  when Array
    expected.map{ |expected_element| format_error(expected_element, value) }*" OR"
  when Module
    "\n- Expected #{value.inspect} to be a #{expected}, but is a #{value.class}"
  when Symbol
    "\n- Expected #{value.inspect} to respond to :#{expected}"
  when Proc
    "\n- Expected #{value.inspect} to return a truthy value for proc #{expected}"
  when Regexp
    "\n- Expected stringified #{value.inspect} to match #{expected.inspect}"
  when Range
    "\n- Expected #{value.inspect} to be included in #{expected.inspect}"
  when true
    "\n- Expected #{value.inspect} to be truthy"
  when false
    "\n- Expected #{value.inspect} to be falsy"
  end
end

.get_method_visibility_or_raise(object, method_name) ⇒ Object



39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/sig.rb', line 39

def self.get_method_visibility_or_raise(object, method_name)
  case
  when object.private_method_defined?(method_name)
    :private
  when object.protected_method_defined?(method_name)
    :protected
  when object.public_method_defined?(method_name)
    :public
  else
    raise ArgumentError, "No method with name :#{method_name} for object #{object.inspect}"
  end
end

.get_or_create_signature_checker(object) ⇒ Object



52
53
54
55
56
57
58
59
60
# File 'lib/sig.rb', line 52

def self.get_or_create_signature_checker(object)
  unless checker = object.instance_variable_get(:@_sig)
    checker = object.instance_variable_set(:@_sig, Module.new)
    def checker.inspect() "#<Sig:#{object_id}>" end
    object.prepend(checker)
  end

  checker
end

.matches?(expected, value) ⇒ Boolean

Returns:

  • (Boolean)


107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/sig.rb', line 107

def self.matches?(expected, value)
  case expected
  when Array
    expected.any?{ |expected_element| matches? expected_element, value }
  when Module
    value.is_a?(expected)
  when Symbol
    value.respond_to?(expected)
  when Proc
    !!expected.call(value)
  when Regexp
    !!(expected =~ String(value))
  when Range
    expected.include?(value)
  when true
    !!value
  when false
    !value
  when nil
    true
  else
    raise ArgumentError, "Invalid signature definition: Unknown behavior #{expected}"
  end
end

.valid_or_formatted_error(expected_argument, argument) ⇒ Object



132
133
134
135
136
# File 'lib/sig.rb', line 132

def self.valid_or_formatted_error(expected_argument, argument)
  if !expected_argument.nil? && !matches?(expected_argument, argument)
    format_error(expected_argument, argument)
  end
end