gorillib-model 0.0.1
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.
- data/.gitignore +4 -0
- data/Gemfile +12 -0
- data/README.md +21 -0
- data/Rakefile +15 -0
- data/gorillib-model.gemspec +27 -0
- data/lib/gorillib/builder.rb +239 -0
- data/lib/gorillib/core_ext/datetime.rb +23 -0
- data/lib/gorillib/core_ext/exception.rb +153 -0
- data/lib/gorillib/core_ext/module.rb +10 -0
- data/lib/gorillib/core_ext/object.rb +14 -0
- data/lib/gorillib/model/base.rb +273 -0
- data/lib/gorillib/model/collection/model_collection.rb +157 -0
- data/lib/gorillib/model/collection.rb +200 -0
- data/lib/gorillib/model/defaults.rb +115 -0
- data/lib/gorillib/model/errors.rb +24 -0
- data/lib/gorillib/model/factories.rb +555 -0
- data/lib/gorillib/model/field.rb +168 -0
- data/lib/gorillib/model/lint.rb +24 -0
- data/lib/gorillib/model/named_schema.rb +53 -0
- data/lib/gorillib/model/positional_fields.rb +35 -0
- data/lib/gorillib/model/schema_magic.rb +163 -0
- data/lib/gorillib/model/serialization/csv.rb +60 -0
- data/lib/gorillib/model/serialization/json.rb +44 -0
- data/lib/gorillib/model/serialization/lines.rb +30 -0
- data/lib/gorillib/model/serialization/to_wire.rb +54 -0
- data/lib/gorillib/model/serialization/tsv.rb +53 -0
- data/lib/gorillib/model/serialization.rb +41 -0
- data/lib/gorillib/model/type/extended.rb +83 -0
- data/lib/gorillib/model/type/ip_address.rb +153 -0
- data/lib/gorillib/model/type/url.rb +11 -0
- data/lib/gorillib/model/validate.rb +22 -0
- data/lib/gorillib/model/version.rb +5 -0
- data/lib/gorillib/model.rb +34 -0
- data/spec/builder_spec.rb +193 -0
- data/spec/core_ext/datetime_spec.rb +41 -0
- data/spec/core_ext/exception.rb +98 -0
- data/spec/core_ext/object.rb +45 -0
- data/spec/model/collection_spec.rb +290 -0
- data/spec/model/defaults_spec.rb +104 -0
- data/spec/model/factories_spec.rb +323 -0
- data/spec/model/lint_spec.rb +28 -0
- data/spec/model/serialization/csv_spec.rb +30 -0
- data/spec/model/serialization/tsv_spec.rb +28 -0
- data/spec/model/serialization_spec.rb +41 -0
- data/spec/model/type/extended_spec.rb +166 -0
- data/spec/model/type/ip_address_spec.rb +141 -0
- data/spec/model_spec.rb +261 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/capture_output.rb +28 -0
- data/spec/support/nuke_constants.rb +9 -0
- data/spec/support/shared_context_for_builders.rb +59 -0
- data/spec/support/shared_context_for_models.rb +55 -0
- data/spec/support/shared_examples_for_factories.rb +71 -0
- data/spec/support/shared_examples_for_model_fields.rb +62 -0
- data/spec/support/shared_examples_for_models.rb +87 -0
- metadata +193 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gemspec
|
4
|
+
|
5
|
+
group :development do
|
6
|
+
gem 'rake', '~> 10.3.2'
|
7
|
+
gem 'yard', '~> 0.8.7'
|
8
|
+
gem 'rspec', '~> 2.14.1'
|
9
|
+
gem 'redcarpet', '~> 3.1.2', platform: [:ruby]
|
10
|
+
gem 'kramdown', '~> 1.4.0', platform: [:jruby]
|
11
|
+
gem 'simplecov', '~> 0.8.2', platform: [:ruby_19], require: false
|
12
|
+
end
|
data/README.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# Gorillib::Model
|
2
|
+
|
3
|
+
## WARNING
|
4
|
+
|
5
|
+
This has been extracted from the open-source library [gorillib](https://github.com/infochimps-labs/gorillib).
|
6
|
+
All functionality for `Gorillib::Model` has been retained, but all helper methods now come from `active_support`.
|
7
|
+
This separation from the old `gorillib` is intentional, as support for that library will no longer continue.
|
8
|
+
Please use this code only as intended, and make no assumptions about old functionality.
|
9
|
+
|
10
|
+
## Usage
|
11
|
+
|
12
|
+
`require 'gorillib/model`
|
13
|
+
|
14
|
+
Gorillib has at least one powerful addition to the canon: the `Gorillib::Model` mixin.
|
15
|
+
|
16
|
+
Think of it like 'An ORM for JSON'. It's designed for data that spends as much time on the wire as it does in action -- things like API handlers or clients, data processing scripts, wukong jobs.
|
17
|
+
|
18
|
+
* lightweight
|
19
|
+
* serializes to/from JSON, TSV or plain hashes
|
20
|
+
* type converts when you need it, but doesn't complicate normal accessors
|
21
|
+
* upward compatible with ActiveModel
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
|
3
|
+
task default: [:spec]
|
4
|
+
|
5
|
+
require 'rspec/core/rake_task'
|
6
|
+
RSpec::Core::RakeTask.new
|
7
|
+
|
8
|
+
desc 'Run RSpec with code coverage'
|
9
|
+
task :cov do
|
10
|
+
ENV['GORILLIB_MODEL_COV'] = 'true'
|
11
|
+
Rake::Task[:spec].execute
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'yard'
|
15
|
+
YARD::Rake::YardocTask.new
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'gorillib/model/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = 'gorillib-model'
|
8
|
+
gem.version = Gorillib::Model::VERSION
|
9
|
+
gem.authors = %w[ Infochimps ]
|
10
|
+
gem.email = 'coders@infochimps.com'
|
11
|
+
gem.homepage = 'https://github.com/infochimps-labs/weavr.git'
|
12
|
+
gem.licenses = ['Apache 2.0']
|
13
|
+
gem.summary = 'Fully-featured Ruby model library'
|
14
|
+
gem.description = <<-DESC.gsub(/^ {4}/, '').chomp
|
15
|
+
Gorillib::Model
|
16
|
+
DESC
|
17
|
+
|
18
|
+
gem.files = `git ls-files`.split($/)
|
19
|
+
gem.test_files = gem.files.grep(/^spec/)
|
20
|
+
gem.require_paths = %w[ lib ]
|
21
|
+
|
22
|
+
gem.add_development_dependency('bundler', '~> 1.6.3')
|
23
|
+
|
24
|
+
gem.add_dependency('multi_json', '~> 1.10.1')
|
25
|
+
gem.add_dependency('activesupport', '~> 4.1.4')
|
26
|
+
gem.add_dependency('addressable', '~> 2.3.6')
|
27
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
require 'gorillib/model'
|
2
|
+
require 'gorillib/model/collection'
|
3
|
+
require 'gorillib/model/collection/model_collection'
|
4
|
+
|
5
|
+
module Gorillib
|
6
|
+
module Builder
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
include Gorillib::Model
|
9
|
+
|
10
|
+
# @return [Object, nil] the return value of the block, or nil if no block given
|
11
|
+
def receive!(*args, &block)
|
12
|
+
super(*args)
|
13
|
+
if block_given?
|
14
|
+
(block.arity == 1) ? block.call(self) : self.instance_eval(&block)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def getset(field, *args, &block)
|
19
|
+
ArgumentError.check_arity!(args, 0..1)
|
20
|
+
if args.empty?
|
21
|
+
read_attribute(field.name)
|
22
|
+
else
|
23
|
+
write_attribute(field.name, args.first)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def getset_member(field, *args, &block)
|
28
|
+
ArgumentError.check_arity!(args, 0..1)
|
29
|
+
attrs = args.first
|
30
|
+
if attrs.is_a?(field.type)
|
31
|
+
# actual object: assign it into field
|
32
|
+
val = attrs
|
33
|
+
write_attribute(field.name, val)
|
34
|
+
else
|
35
|
+
val = read_attribute(field.name)
|
36
|
+
if val.present?
|
37
|
+
# existing item: update it with args and block
|
38
|
+
val.receive!(*args, &block) if args.present? or block_given?
|
39
|
+
elsif attrs.blank? and not block_given?
|
40
|
+
# missing item (read): return nil
|
41
|
+
return nil
|
42
|
+
else
|
43
|
+
# missing item (write): construct item and add to collection
|
44
|
+
options = args.extract_options!.merge(:owner => self)
|
45
|
+
val = field.type.receive(*args, options, &block)
|
46
|
+
write_attribute(field.name, val)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
val
|
50
|
+
end
|
51
|
+
|
52
|
+
def getset_collection_item(field, item_key, attrs={}, &block)
|
53
|
+
plural_name = field.plural_name
|
54
|
+
if attrs.is_a?(field.item_type)
|
55
|
+
# actual object: assign it into collection
|
56
|
+
val = attrs
|
57
|
+
set_collection_item(plural_name, item_key, val)
|
58
|
+
elsif has_collection_item?(plural_name, item_key)
|
59
|
+
# existing item: retrieve it, updating as directed
|
60
|
+
val = get_collection_item(plural_name, item_key)
|
61
|
+
val.receive!(attrs, &block)
|
62
|
+
else
|
63
|
+
# missing item: autovivify item and add to collection
|
64
|
+
val = field.item_type.receive({ key_method => item_key, :owner => self }.merge(attrs), &block)
|
65
|
+
set_collection_item(plural_name, item_key, val)
|
66
|
+
end
|
67
|
+
val
|
68
|
+
end
|
69
|
+
|
70
|
+
def get_collection_item(plural_name, item_key)
|
71
|
+
collection_of(plural_name)[item_key]
|
72
|
+
end
|
73
|
+
|
74
|
+
def set_collection_item(plural_name, item_key, item)
|
75
|
+
collection = collection_of(plural_name)
|
76
|
+
collection[item_key] = item
|
77
|
+
collection[item_key]
|
78
|
+
end
|
79
|
+
|
80
|
+
def has_collection_item?(plural_name, item_key)
|
81
|
+
collection_of(plural_name).include?(item_key)
|
82
|
+
end
|
83
|
+
|
84
|
+
def key_method
|
85
|
+
:name
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_key
|
89
|
+
self.send(key_method)
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_inspectable
|
93
|
+
super.tap{|attrs| attrs.delete(:owner) }
|
94
|
+
end
|
95
|
+
|
96
|
+
def collection_of(plural_name)
|
97
|
+
self.read_attribute(plural_name)
|
98
|
+
end
|
99
|
+
|
100
|
+
module ClassMethods
|
101
|
+
include Gorillib::Model::ClassMethods
|
102
|
+
|
103
|
+
def magic(field_name, type, options={})
|
104
|
+
field(field_name, type, {:field_type => ::Gorillib::Builder::GetsetField}.merge(options))
|
105
|
+
end
|
106
|
+
def member(field_name, type, options={})
|
107
|
+
field(field_name, type, {:field_type => ::Gorillib::Builder::MemberField}.merge(options))
|
108
|
+
end
|
109
|
+
|
110
|
+
# FIXME: this interface is borked -- it should not take the item_type in the second slot.
|
111
|
+
def collection(field_name, item_type, options={})
|
112
|
+
super(field_name, Gorillib::ModelCollection, {
|
113
|
+
:item_type => item_type, :field_type => ::Gorillib::Builder::GetsetCollectionField }.merge(options))
|
114
|
+
end
|
115
|
+
|
116
|
+
protected
|
117
|
+
|
118
|
+
def define_attribute_getset(field)
|
119
|
+
field_name = field.name; type = field.type
|
120
|
+
define_meta_module_method(field_name, field.visibility(:reader)) do |*args, &block|
|
121
|
+
begin
|
122
|
+
getset(field, *args, &block)
|
123
|
+
rescue StandardError => err ; err.polish("#{self.class}.#{field_name} type #{type} on #{args}'") rescue nil ; raise ; end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def define_member_getset(field)
|
128
|
+
field_name = field.name; type = field.type
|
129
|
+
define_meta_module_method(field_name, field.visibility(:reader)) do |*args, &block|
|
130
|
+
begin
|
131
|
+
getset_member(field, *args, &block)
|
132
|
+
rescue StandardError => err ; err.polish("#{self.class}.#{field_name} type #{type} on #{args}'") rescue nil ; raise ; end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def define_collection_getset(field)
|
137
|
+
field_name = field.name; item_type = field.item_type
|
138
|
+
define_meta_module_method(field.singular_name, field.visibility(:collection_getset)) do |*args, &block|
|
139
|
+
begin
|
140
|
+
getset_collection_item(field, *args, &block)
|
141
|
+
rescue StandardError => err ; err.polish("#{self.class}.#{field_name} c[#{item_type}] on #{args}'") rescue nil ; raise ; end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def define_collection_tester(field)
|
146
|
+
plural_name = field.plural_name
|
147
|
+
define_meta_module_method("has_#{field.singular_name}?", field.visibility(:collection_tester)) do |item_key|
|
148
|
+
begin
|
149
|
+
collection_of(plural_name).include?(item_key)
|
150
|
+
rescue StandardError => err ; err.polish("#{self.class}.#{plural_name} having #{item_key}?'") rescue nil ; raise ; end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
module FancyBuilder
|
158
|
+
extend ActiveSupport::Concern
|
159
|
+
include Gorillib::Builder
|
160
|
+
|
161
|
+
included do |base|
|
162
|
+
base.magic :name, Symbol
|
163
|
+
end
|
164
|
+
|
165
|
+
module ClassMethods
|
166
|
+
include Gorillib::Builder::ClassMethods
|
167
|
+
|
168
|
+
def belongs_to(field_name, type, options={})
|
169
|
+
field = member(field_name, type)
|
170
|
+
define_meta_module_method "#{field.name}_name" do
|
171
|
+
val = getset_member(field) or return nil
|
172
|
+
val.name
|
173
|
+
end
|
174
|
+
field
|
175
|
+
end
|
176
|
+
|
177
|
+
def option(field_name, options={})
|
178
|
+
type = options.delete(:type){ Whatever }
|
179
|
+
magic(field_name, type)
|
180
|
+
end
|
181
|
+
|
182
|
+
def collects(type, clxn_name)
|
183
|
+
type_handle = type.handle
|
184
|
+
define_meta_module_method type_handle do |item_name, attrs={}, options={}, &block|
|
185
|
+
send(clxn_name, item_name, attrs, options.merge(:factory => type), &block)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
module Builder
|
192
|
+
|
193
|
+
class GetsetField < Gorillib::Model::Field
|
194
|
+
self.visibilities = visibilities.merge(:writer => false, :tester => false, :getset => true)
|
195
|
+
def inscribe_methods(model)
|
196
|
+
model.__send__(:define_attribute_getset, self)
|
197
|
+
model.__send__(:define_attribute_writer, self.name, self.type, visibility(:writer))
|
198
|
+
model.__send__(:define_attribute_tester, self.name, self.type, visibility(:tester))
|
199
|
+
model.__send__(:define_attribute_receiver, self.name, self.type, visibility(:receiver))
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
class MemberField < Gorillib::Model::Field
|
204
|
+
self.visibilities = visibilities.merge(:writer => false, :tester => true)
|
205
|
+
def inscribe_methods(model)
|
206
|
+
model.__send__(:define_member_getset, self)
|
207
|
+
model.__send__(:define_attribute_writer, self.name, self.type, visibility(:writer))
|
208
|
+
model.__send__(:define_attribute_tester, self.name, self.type, visibility(:tester))
|
209
|
+
model.__send__(:define_attribute_receiver, self.name, self.type, visibility(:receiver))
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
class GetsetCollectionField < ::Gorillib::Model::SimpleCollectionField
|
214
|
+
field :singular_name, Symbol, :default => ->{ ActiveSupport::Inflector.singularize(name.to_s).to_sym }
|
215
|
+
|
216
|
+
self.visibilities = visibilities.merge(:writer => false, :tester => false,
|
217
|
+
:collection_getset => :public, :collection_tester => true)
|
218
|
+
|
219
|
+
alias_method :plural_name, :name
|
220
|
+
def singular_name
|
221
|
+
@singular_name ||= ActiveSupport::Inflector.singularize(name.to_s).to_sym
|
222
|
+
end
|
223
|
+
|
224
|
+
def inscribe_methods(model)
|
225
|
+
raise "Plural and singular names must differ: #{self.plural_name}" if (singular_name == plural_name)
|
226
|
+
#
|
227
|
+
@visibilities[:writer] = false
|
228
|
+
model.__send__(:define_attribute_reader, self.name, self.type, visibility(:reader))
|
229
|
+
model.__send__(:define_attribute_tester, self.name, self.type, visibility(:tester))
|
230
|
+
#
|
231
|
+
model.__send__(:define_collection_receiver, self)
|
232
|
+
model.__send__(:define_collection_getset, self)
|
233
|
+
model.__send__(:define_collection_tester, self)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
CollectionField = GetsetCollectionField
|
237
|
+
|
238
|
+
end
|
239
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
Time.class_eval do
|
2
|
+
#
|
3
|
+
# Parses the time but never fails.
|
4
|
+
# Return value is always in the UTC time zone.
|
5
|
+
#
|
6
|
+
# A flattened datetime -- a 14-digit YYYYmmddHHMMMSS -- is fixed to the UTC
|
7
|
+
# time zone by parsing it as YYYYmmddHHMMMSSZ <- 'Z' at end
|
8
|
+
#
|
9
|
+
def self.parse_safely dt
|
10
|
+
return nil if dt.nil? || (dt.respond_to?(:empty) && dt.empty?)
|
11
|
+
begin
|
12
|
+
case
|
13
|
+
when dt.is_a?(Time) then dt.utc
|
14
|
+
when (dt.to_s =~ /\A\d{14}\z/) then parse(dt.to_s+'Z', true)
|
15
|
+
else parse(dt.to_s, true).utc
|
16
|
+
end
|
17
|
+
rescue StandardError => err
|
18
|
+
warn "Can't parse a #{self} from #{dt.inspect}"
|
19
|
+
warn err
|
20
|
+
return nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
Exception.class_eval do
|
2
|
+
# @return [Array] file, line, method_name
|
3
|
+
def self.caller_parts(depth=1)
|
4
|
+
caller_line = caller(depth).first
|
5
|
+
mg = %r{\A([^:]+):(\d+):in \`([^\']+)\'\z}.match(caller_line) or return [caller_line, 1, '(unknown)']
|
6
|
+
[mg[1], mg[2].to_i, mg[3]]
|
7
|
+
rescue
|
8
|
+
warn "problem in #{self}.caller_parts"
|
9
|
+
return [__FILE__, __LINE__, '(unknown)']
|
10
|
+
end
|
11
|
+
|
12
|
+
#
|
13
|
+
# Add context to the backtrace of exceptions ocurring downstream from caller.
|
14
|
+
# This is expecially useful in metaprogramming. Follow the implementation in
|
15
|
+
# the example.
|
16
|
+
#
|
17
|
+
# @note !! Be sure to rescue the call to this method; few things suck worse
|
18
|
+
# than debugging your rescue blocks.
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# define_method(:cromulate) do |level|
|
22
|
+
# begin
|
23
|
+
# adjust_cromulance(cromulator, level)
|
24
|
+
# rescue StandardError => err ; err.polish("setting cromulance #{level} for #{cromulator}") rescue nil ; raise ; end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
def polish(extra_info)
|
28
|
+
filename, _, method_name = self.class.caller_parts(2)
|
29
|
+
method_name.gsub!(/rescue in /, '')
|
30
|
+
most_recent_line = backtrace.detect{|line|
|
31
|
+
line.include?(filename) && line.include?(method_name) && line.end_with?("'") }
|
32
|
+
most_recent_line.sub!(/'$/, "' for [#{extra_info.to_s[0..300]}]")
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
ArgumentError.class_eval do
|
38
|
+
# Raise an error if there are a different number of arguments than expected.
|
39
|
+
# The message will have the same format used by Ruby internal methods.
|
40
|
+
# @see #arity_at_least!
|
41
|
+
#
|
42
|
+
# @example want `getset(:foo)` to be different from `getset(:foo, nil)`
|
43
|
+
# def getset(key, *args)
|
44
|
+
# ArgumentError.check_arity!(args, 0..1)
|
45
|
+
# return self[key] if args.empty?
|
46
|
+
# self[key] = args.first
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# @overload check_arity!(args, n)
|
50
|
+
# @param [Array] args splat args as handed to the caller
|
51
|
+
# @param [Integer] val expected length
|
52
|
+
# @overload check_arity!(args, x..y)
|
53
|
+
# @param [Array] args splat args as handed to the caller
|
54
|
+
# @param [#include?] val expected range/list/set of lengths
|
55
|
+
# @raise ArgumentError when there are
|
56
|
+
def self.check_arity!(args, val, &block)
|
57
|
+
allowed_arity = val.is_a?(Integer) ? (val..val) : val
|
58
|
+
return true if allowed_arity.include?(args.length)
|
59
|
+
info = " #{block.call}" rescue nil if block_given?
|
60
|
+
raise self.new("wrong number of arguments (#{args.length} for #{val})#{info}")
|
61
|
+
end
|
62
|
+
|
63
|
+
# Raise an error if there are fewer arguments than expected. The message will
|
64
|
+
# have the same format used by Ruby internal methods.
|
65
|
+
# @see #check_arity!
|
66
|
+
#
|
67
|
+
# @example want to use splat args, requiring at least one
|
68
|
+
# def assemble_path(*pathsegs)
|
69
|
+
# ArgumentError.arity_at_least!(pathsegs, 1)
|
70
|
+
# # ...
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# @param [Array] args splat args as handed to the caller
|
74
|
+
# @param [Integer] val minimum expected length
|
75
|
+
def self.arity_at_least!(args, min_arity)
|
76
|
+
check_arity!(args, min_arity .. Float::INFINITY)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class TypeMismatchError < ArgumentError ; end
|
81
|
+
|
82
|
+
class ArgumentError
|
83
|
+
#
|
84
|
+
# @param [Array[Symbol,Class,Module]] types
|
85
|
+
#
|
86
|
+
# @example simple
|
87
|
+
# TypeMismatchError.mismatched!(:foo)
|
88
|
+
# #=> "TypeMismatchError: :foo has mismatched type
|
89
|
+
#
|
90
|
+
# @example Can supply the types or duck-types that are expected:
|
91
|
+
# TypeMismatchError.mismatched!(:foo, [:to_str, Integer])
|
92
|
+
# #=> "TypeMismatchError: :foo has mismatched type; expected #to_str or Integer"
|
93
|
+
#
|
94
|
+
def self.mismatched!(obj, types=[], msg=nil, *args)
|
95
|
+
types = Array(types)
|
96
|
+
message = (obj.inspect rescue '(uninspectable object)')
|
97
|
+
message << " has mismatched type"
|
98
|
+
message << ': ' << msg if msg
|
99
|
+
unless types.empty?
|
100
|
+
message << '; expected ' << types.map{|type| type.is_a?(Symbol) ? "##{type}" : type.to_s }.join(" or ")
|
101
|
+
end
|
102
|
+
raise self, message, *args
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
def self.block_required!(block)
|
107
|
+
raise self.new("Block is required") unless block
|
108
|
+
end
|
109
|
+
|
110
|
+
#
|
111
|
+
# @param obj [Object] Object to check
|
112
|
+
# @param types [Array[Symbol,Class,Module]] Types or methods to compare
|
113
|
+
#
|
114
|
+
# @example simple
|
115
|
+
# TypeMismatchError.mismatched!(:foo)
|
116
|
+
# #=> "TypeMismatchError: :foo has mismatched type
|
117
|
+
#
|
118
|
+
# @example Can supply the types or duck-types that are expected:
|
119
|
+
# TypeMismatchError.mismatched!(:foo, [:to_str, Integer])
|
120
|
+
# #=> "TypeMismatchError: :foo has mismatched type; expected #to_str or Integer"
|
121
|
+
#
|
122
|
+
def self.check_type!(obj, types, *args)
|
123
|
+
types = Array(types)
|
124
|
+
return true if types.any? do |type|
|
125
|
+
case type
|
126
|
+
when Module then obj.is_a?(type)
|
127
|
+
when Symbol then obj.respond_to?(type)
|
128
|
+
else raise StandardError, "Can't check type #{type} -- this is an error in the call to the type-checker, not in the object the type-checker is checking"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
self.mismatched!(obj, types, *args)
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
#
|
137
|
+
class AbstractMethodError < NoMethodError ; end
|
138
|
+
|
139
|
+
NoMethodError.class_eval do
|
140
|
+
MESSAGE_FMT = "undefined method `%s' for %s:%s"
|
141
|
+
|
142
|
+
# Raise an error with the same format used by Ruby internal methods
|
143
|
+
def self.undefined_method!(obj)
|
144
|
+
file, line, meth = caller_parts
|
145
|
+
raise self.new(MESSAGE_FMT % [meth, obj, obj.class])
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.abstract_method!(obj)
|
149
|
+
file, line, meth = caller_parts
|
150
|
+
raise AbstractMethodError.new("#{MESSAGE} -- must be implemented by the subclass" % [meth, obj, obj.class])
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# It is necessary to override this ActiveSupport method
|
2
|
+
# due to the differences between #remove_method and #undef_method
|
3
|
+
Module.class_eval do
|
4
|
+
def remove_possible_method(method)
|
5
|
+
if method_defined?(method) || private_method_defined?(method)
|
6
|
+
remove_method(method)
|
7
|
+
end
|
8
|
+
rescue NameError
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
Object.class_eval do
|
2
|
+
# Override this in a child if it cannot be dup'ed
|
3
|
+
#
|
4
|
+
# @return [Object]
|
5
|
+
def try_dup
|
6
|
+
self.dup
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
[ TrueClass, FalseClass, Module, NilClass, Numeric, Symbol ].each do |klass|
|
11
|
+
klass.class_eval do
|
12
|
+
def try_dup() self ; end
|
13
|
+
end
|
14
|
+
end
|