mousevc 0.0.2.pre.alpha

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,35 @@
1
+ require_relative 'validation.rb'
2
+
3
+ module Mousevc
4
+
5
+ ##
6
+ # The base model class for a Mousevc application.
7
+ # Provides access to basic functionality for validating input before modifying data
8
+ # via it's +@validation+ attribute, an instance of the +Mousevc::Validation+ class.
9
+
10
+ class Model
11
+
12
+ ##
13
+ # @!attribute [r]
14
+ # @return [Mousevc::Validation] a reference to the model's validation instance
15
+
16
+ attr_reader :validation
17
+
18
+ ##
19
+ # Creates a new +Mousevc::Model+ instance
20
+ #
21
+ # @param options [Hash] optionally accepts the following keys:
22
+ # - :validation => [Mousevc::Router] an instance of the +Mousevc::Validation+ class
23
+
24
+ def initialize(options={})
25
+ @validation = options[:validation] ? options[:validation] : Validation.new
26
+ clear
27
+ end
28
+
29
+ ##
30
+ # Overridable empty method for clearing data in the model subclass
31
+
32
+ def clear
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,52 @@
1
+ module Mousevc
2
+
3
+ ##
4
+ # The +Persistance+ class enables storage and retrieval of models.
5
+ # While it is intended for storing models it can store
6
+ # any value as internally it stores the value in a Ruby Hash.
7
+ #
8
+ # @note The +Persistance+ class only stores data during the application life cycle. Once the application quits the data may still exist in memory if not explicitly cleared, however it should not be relied upon to be their for subsequent executions.
9
+
10
+ class Persistence
11
+
12
+ ##
13
+ # Internal data storage
14
+
15
+ @@models = {}
16
+
17
+ ##
18
+ # Allows clearing of all or some of the models currently stored.
19
+ # Clears all data and models stored if no keys are provided
20
+ #
21
+ # @param args [Symbol] a list of keys to clear from the currently stored models
22
+
23
+ def self.clear(*args)
24
+ if args.empty?
25
+ @@models.clear
26
+ else
27
+ args.each {|arg| @@models.delete(arg)}
28
+ end
29
+ end
30
+
31
+ ##
32
+ # Get a stored model by key
33
+ #
34
+ # @param key [Symbol] the key under which the model is stored
35
+ # @return [Mousevc::Model, Any] the stored model or value
36
+
37
+ def self.get(key)
38
+ @@models[key]
39
+ end
40
+
41
+ ##
42
+ # Set the value of a storage key
43
+ # @raise [Mousevc::Error] if the key already exists
44
+ #
45
+ # @param key [Symbol] the key under which the model is stored
46
+
47
+ def self.set(key, value)
48
+ raise Error.new("Cannot persist, a value already exists at: #{key}") unless @@models[key].nil?
49
+ @@models.store(key, value)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,77 @@
1
+ require_relative 'controller.rb'
2
+ require_relative 'model.rb'
3
+ require_relative 'view.rb'
4
+
5
+ module Mousevc
6
+
7
+ ##
8
+ # Router routes requests to the controller method. Instantiated by the +Mousevc::App+ class.
9
+
10
+ class Router
11
+
12
+ ##
13
+ # @!attribute controller
14
+ #
15
+ # @note Set this variable to the string name of the controller class to be instantiated upon the next application loop
16
+ #
17
+ # @return [String] the instance of the current controller
18
+
19
+ attr_accessor :controller
20
+
21
+ # @!attribute model
22
+ #
23
+ # @note Set this variable to the string name of the model class to be instantiated upon the next application loop
24
+ # @note Can be used to retrieve a model from the +Mousevc::Persistence+ class when set to a symbol.
25
+ #
26
+ # @return [String, Symbol] the instance of the current model.
27
+
28
+ attr_accessor :model
29
+
30
+ # @!attribute action
31
+ #
32
+ # @note Set this variable to the symbol name of the method to call on the next controller
33
+ #
34
+ # @return [Symbol] the action sent to the controller method
35
+
36
+ attr_accessor :action
37
+
38
+ ##
39
+ # Creates a new +Mousevc::Router+ instance
40
+ #
41
+ # @param options [Hash] expects the following keys:
42
+ # - :controller => [String] name of default controller class
43
+ # - :model => [String] name of default model class
44
+ # - :action => [Symbol] method to call on default controller
45
+ # - :views => [String] relative path to views directory
46
+
47
+ def initialize(options={})
48
+ @controller = options[:controller] ? options[:controller] : 'Controller'
49
+ @model = options[:model] ? options[:model] : 'Model'
50
+ @action = options[:action] ? options[:action] : :hello_mousevc
51
+ @views = options[:views]
52
+ end
53
+
54
+ ##
55
+ # Routes by:
56
+ #
57
+ # 1. creating an instance of the current controller in the +@controller+ attribute
58
+ # 1. creating an instance of the current model in the +@model+ attribute
59
+ # 1. creating a new or finding the desired model
60
+ # 1. passing that controller that model instance, a view instance, and an instance of +self+
61
+ # 1. sending the controller the current action in +@action+
62
+
63
+ def route
64
+ model = nil
65
+ model = Persistence.get(@model) if @model.is_a?(Symbol)
66
+ model = Persistence.get(@controller.to_sym) unless Persistence.get(@controller.to_sym).nil?
67
+ model = Mousevc.factory(@model).new unless model
68
+ view = View.new(:dir => @views)
69
+ controller = Mousevc.factory(@controller).new(
70
+ :view => view,
71
+ :model => model,
72
+ :router => self
73
+ )
74
+ controller.send(@action)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,353 @@
1
+ module Mousevc
2
+
3
+ ##
4
+ # Extend this class to perform validations on user input in your model.
5
+ # Use the +@error+ attribute to pass notices to the +Mousevc::Input+ class.
6
+ # From there the notice can be easily output in your views.
7
+ #
8
+ # Each predicate method in this class sets the appropriate error message ready
9
+ # to be output to the user.
10
+
11
+ class Validation
12
+
13
+ ##
14
+ # @!attribute error
15
+ #
16
+ # @return [String] the error message generated during validation
17
+
18
+ attr_accessor :error
19
+
20
+ ##
21
+ # Returns +true+ if the string containers only uppercase or lowercase letters
22
+ #
23
+ # @param value [String] the value
24
+ # @return [Boolean]
25
+
26
+ def alpha?(value)
27
+ is_alpha = coerce_bool (value =~ /^[a-zA-Z]+$/)
28
+ unless is_alpha
29
+ @error = "Error, expected only alphabetical characters, got: #{value}"
30
+ end
31
+ is_alpha
32
+ end
33
+
34
+ ##
35
+ # Returns +true+ if the value is equal to the other
36
+ #
37
+ # @param value [Any] the value
38
+ # @param other [Any] the other value
39
+ # @return [Boolean]
40
+
41
+ def matches?(value, other)
42
+ is_match = coerce_bool (value == other)
43
+ unless is_match
44
+ @error = "Error, expected value to match: #{other}, got: #{value}"
45
+ end
46
+ is_match
47
+ end
48
+
49
+ ##
50
+ # Returns +true+ if the value is not equal to the other
51
+ #
52
+ # @param value [Any] the value
53
+ # @param other [Any] the other value
54
+ # @return [Boolean]
55
+
56
+ def differs?(value, other)
57
+ is_different = coerce_bool (value != other)
58
+ unless is_different
59
+ @error = "Error, expected value to differ from: #{other}, got: #{value}"
60
+ end
61
+ is_different
62
+ end
63
+
64
+ ##
65
+ # Returns +true+ if the value has at least the specified length
66
+ #
67
+ # @param value [String] the value
68
+ # @param length [Integer] the length
69
+ # @return [Boolean]
70
+
71
+ def min_length?(value, length)
72
+ has_min_length = coerce_bool (value.length >= length)
73
+ unless has_min_length
74
+ @error = "Error, expected value to have at least #{length} characters, got: #{value.length}"
75
+ end
76
+ has_min_length
77
+ end
78
+
79
+ ##
80
+ # Returns +true+ if the value has at most the specified length
81
+ #
82
+ # @param value [String] the value
83
+ # @param length [Integer] the length
84
+ # @return [Boolean]
85
+
86
+ def max_length?(value, length)
87
+ has_max_length = coerce_bool (value.length <= length)
88
+ unless has_max_length
89
+ @error = "Error, expected value to have at most #{length} characters, got: #{value.length}"
90
+ end
91
+ has_max_length
92
+ end
93
+
94
+ ##
95
+ # Returns +true+ if the value has exactly the specified length
96
+ #
97
+ # @param value [String] the value
98
+ # @param length [Integer] the length
99
+ # @return [Boolean]
100
+
101
+ def exact_length?(value, length)
102
+ has_exact_length = coerce_bool (value.length == length)
103
+ unless has_exact_length
104
+ @error = "Error, expected value to have exactly #{length} characters, got: #{value.length}"
105
+ end
106
+ has_exact_length
107
+ end
108
+
109
+ ##
110
+ # Returns +true+ if the value is greater than the other given value
111
+ # @note This method does note covert strings to numbers
112
+ #
113
+ # @param value [Mixed] the value
114
+ # @param other [Mixed] the other value
115
+ # @return [Boolean]
116
+
117
+ def greater_than?(value, other)
118
+ is_greater = coerce_bool (value > other)
119
+ unless is_greater
120
+ @error = "Error, expected value to be greater than #{other}, got: #{value}"
121
+ end
122
+ is_greater
123
+ end
124
+
125
+ ##
126
+ # Returns +true+ if the value is greater than or equal to the other given value
127
+ # @note This method does note covert strings to numbers
128
+ #
129
+ # @param value [Mixed] the value
130
+ # @param other [Mixed] the other value
131
+ # @return [Boolean]
132
+
133
+ def greater_than_equal_to?(value, other)
134
+ is_greater_eq = coerce_bool (value >= other)
135
+ unless is_greater_eq
136
+ @error = "Error, expected value to be greater than or equal to #{other}, got: #{value}"
137
+ end
138
+ is_greater_eq
139
+ end
140
+
141
+ ##
142
+ # Returns +true+ if the value is less than the other given value
143
+ # @note This method does note covert strings to numbers
144
+ #
145
+ # @param value [Mixed] the value
146
+ # @param other [Mixed] the other value
147
+ # @return [Boolean]
148
+
149
+ def less_than?(value, other)
150
+ is_less = coerce_bool (value < other)
151
+ unless is_less
152
+ @error = "Error, expected value to be less than #{other}, got: #{value}"
153
+ end
154
+ is_less
155
+ end
156
+
157
+ ##
158
+ # Returns +true+ if the value is less than or equal to the other given value
159
+ # @note This method does note covert strings to numbers
160
+ #
161
+ # @param value [Mixed] the value
162
+ # @param other [Mixed] the other value
163
+ # @return [Boolean]
164
+
165
+ def less_than_equal_to?(value, other)
166
+ is_less_eq = coerce_bool (value <= other)
167
+ unless is_less_eq
168
+ @error = "Error, expected value to be less than or equal to #{other}, got: #{value}"
169
+ end
170
+ is_less_eq
171
+ end
172
+
173
+ ##
174
+ # Returns +true+ if the value is an integer or a decimal, includes positive and negative
175
+ #
176
+ # @param value [String] the value
177
+ # @return [Boolean]
178
+
179
+ def numeric?(value)
180
+ is_numeric = (integer?(value) || decimal?(value))
181
+ unless is_numeric
182
+ @error = "Error, expected value to be numeric, got: #{value}"
183
+ end
184
+ is_numeric
185
+ end
186
+
187
+ ##
188
+ # Returns +true+ if the value is an integer, includes positive and negative
189
+ #
190
+ # @param value [String] the value
191
+ # @return [Boolean]
192
+
193
+ def integer?(value)
194
+ is_integer = coerce_bool (value =~ /^[-+]?\d+$/)
195
+ unless is_integer
196
+ @error = "Error, expected value to be an integer, got: #{value}"
197
+ end
198
+ is_integer
199
+ end
200
+
201
+ ##
202
+ # Returns +true+ if the value is an decimal, includes positive and negative
203
+ #
204
+ # @param value [String] the value
205
+ # @return [Boolean]
206
+
207
+ def decimal?(value)
208
+ is_decimal = coerce_bool (value =~ /[+-]?[\d_]+\.[\d_]+(e[+-]?[\d_]+)?\b|[+-]?[\d_]+e[+-]?[\d_]+\b/i)
209
+ unless is_decimal
210
+ @error = "Error, expected value to be a decimal, got: #{value}"
211
+ end
212
+ is_decimal
213
+ end
214
+
215
+ ##
216
+ # Returns +true+ if the value is a positive integer
217
+ #
218
+ # @param value [String] the value
219
+ # @return [Boolean]
220
+
221
+ def natural?(value)
222
+ is_natural = coerce_bool (value =~ /^\d+$/)
223
+ unless is_natural
224
+ @error = "Error, expected value to be a natural number, got: #{value}"
225
+ end
226
+ is_natural
227
+ end
228
+
229
+ ##
230
+ # Returns +true+ if the value is a positive integer greater than zero
231
+ #
232
+ # @param value [String] the value
233
+ # @return [Boolean]
234
+
235
+ def natural_no_zero?(value)
236
+ is_natural_no_zero = coerce_bool (value =~ /^[1-9]+$/)
237
+ unless is_natural_no_zero
238
+ @error = "Error, expected value to be a natural number above 0, got: #{value}"
239
+ end
240
+ is_natural_no_zero
241
+ end
242
+
243
+ ##
244
+ # Returns +true+ if the value is a valid url and accounts for many edge cases
245
+ #
246
+ # @param value [String] the value
247
+ # @return [Boolean]
248
+
249
+ def url?(value)
250
+ is_url = coerce_bool (value =~ /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-{1})*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-{1})*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[\/?#]\S*)?$/i)
251
+ unless is_url
252
+ @error = "Error, expected value to be a url, got: #{value}"
253
+ end
254
+ is_url
255
+ end
256
+
257
+ ##
258
+ # Returns +true+ if the value is a valid email address
259
+ #
260
+ # @note This method is more a validation that the value is probably an email. The best way to confirm an email is to require the user to enter it twice, compare them, and ultimately send them an email!
261
+ #
262
+ # @param value [String] the value
263
+ # @return [Boolean]
264
+
265
+ def email?(value)
266
+ is_email = coerce_bool (value =~ /.+@.+/)
267
+ unless is_email
268
+ @error = "Error, expected value to be an email address, got: #{value}"
269
+ end
270
+ is_email
271
+ end
272
+
273
+ ##
274
+ # Returns +true+ if the value is a valid IP address i.e. between 0.0.0.0 and 255.255.255.255
275
+ #
276
+ # @param value [String] the value
277
+ # @return [Boolean]
278
+
279
+ def ip?(value)
280
+ is_ip = coerce_bool (value =~ /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/)
281
+ unless is_ip
282
+ @error = "Error, expected value to be an IP address, got: #{value}"
283
+ end
284
+ is_ip
285
+ end
286
+
287
+ ##
288
+ # Returns +true+ if the value contains only letters, numbers
289
+ # and has a length between the min and max of the given range
290
+ #
291
+ # @param value [String] the value
292
+ # @param range [Range] the min and max length of the password, defaults to +8..15+
293
+ # @return [Boolean]
294
+
295
+ def password?(value, range=8..15)
296
+ is_password = coerce_bool (value =~ /^(?!.*["'])(?=.*\d)(?=.*[a-z])\S{#{range.min},#{range.max}}$/)
297
+ unless is_password
298
+ @error = "Error, expected value to contain only lowercase letters, numbers, and have length between #{range.min} and #{range.max}, got: #{value}"
299
+ end
300
+ is_password
301
+ end
302
+
303
+ ##
304
+ # Returns +true+ if the value contains only letters, numbers and has a length,
305
+ # has a length between the min and max of the given range,
306
+ # and contains at least one uppercase letter
307
+ #
308
+ # @param value [String] the value
309
+ # @param range [Range] the min and max length of the password, defaults to +8..15+
310
+ # @return [Boolean]
311
+
312
+ def password_caps?(value, range=8..15)
313
+ is_password_caps = coerce_bool (value =~ /^(?!.*["'])(?=.*\d)(?=.*[A-Z])(?=.*[a-z])\S{#{range.min},#{range.max}}$/)
314
+ unless is_password_caps
315
+ @error = "Error, expected value to contain lowercase letters, numbers, have length between #{range.min} and #{range.max} and at least one uppercase letter, got: #{value}"
316
+ end
317
+ is_password_caps
318
+ end
319
+
320
+ ##
321
+ # Returns +true+ if the value contains only letters, numbers and has a length,
322
+ # has a length between the min and max of the given range,
323
+ # contains at least one uppercase letter,
324
+ # and contains at least one symbol
325
+ #
326
+ # @note Excludes single and double quotes.
327
+ #
328
+ # @param value [String] the value
329
+ # @param range [Range] the min and max length of the password, defaults to +8..15+
330
+ # @return [Boolean]
331
+
332
+ def password_symbols?(value, range=8..15)
333
+ is_password_symbols = coerce_bool (value =~ /^(?!.*["'])(?=.*[~!@#$%^&*()_+\-\\|{}<>\[\]:;?\/])(?=.*\d)(?=.*[A-Z])(?=.*[a-z])\S{#{range.min},#{range.max}}$/)
334
+ unless is_password_symbols
335
+ @error = @error = "Error, expected value to contain lowercase letters, numbers, have length between #{range.min} and #{range.max}, at least one uppercase letter and at least one symbol, got: #{value}"
336
+ end
337
+ is_password_symbols
338
+ end
339
+
340
+ protected
341
+
342
+ ##
343
+ # A method used to guarantee a value is converted explicitly into either +true+ or +false+
344
+ #
345
+ # @param value [Any] the original value
346
+ # @return [Boolean]
347
+
348
+ def coerce_bool(value)
349
+ !!value
350
+ end
351
+
352
+ end
353
+ end
@@ -0,0 +1,7 @@
1
+ module Mousevc
2
+
3
+ ##
4
+ # The current version
5
+
6
+ VERSION = "0.0.2-alpha"
7
+ end
@@ -0,0 +1,66 @@
1
+ require 'erb'
2
+
3
+ module Mousevc
4
+
5
+ ##
6
+ # The view rendering class of Mousevc.
7
+ # @note Currently only supports ERB templates and a file naming convention of: +[VIEW_NAME].txt.erb+
8
+
9
+ class View
10
+
11
+ ##
12
+ # @!attribute dir
13
+ #
14
+ # @return [String] the path to the views directory
15
+
16
+ attr_reader :dir
17
+
18
+ ##
19
+ # Create a new +Mousevc::View+ instance
20
+
21
+ def initialize(options={})
22
+ @dir = options[:dir]
23
+ end
24
+
25
+ ##
26
+ # Renders a view, passing it the given data. In the ERB template the hash variables will be available as instance variables e.g. +@view_variable+
27
+ #
28
+ # @note If the string passed to the +view+ parameter is an existing file it will be used as the ERB template. Otherwise the string will be parsed as ERB.
29
+ #
30
+ # @note Optionally the view output can be suppressed via setting output to +false+. The view will be returned as a string allowing later output e.g. +render('view', {:data => data}, false)+
31
+ #
32
+ # @note In the event that you want to suppress output and not provide any data you may substitute +data+ for +output+ in the parameter list e.g. +render('view', false)+
33
+ #
34
+ # @param view [String] the name of the view with +.txt.erb+ omitted
35
+ # @param args [Array] accepts 2 additional parameters.
36
+ # - +data+ [Hash] the data to pass to the view (optionally omit)
37
+ # - +output+ [Boolean] false if output is to be suppressed
38
+ #
39
+ # @return [String] the rendered view as a string
40
+
41
+ def render(view, *args)
42
+ data = args[0].is_a?(Hash) ? args[0] : {}
43
+ output = true
44
+ output = false if args[0] == false || args[1] == false
45
+ path = "#{Dir.pwd}/#{@dir}/#{view}.txt.erb"
46
+ view = File.file?(path) ? File.read(path) : view
47
+ to_ivars(data)
48
+ result = ERB.new(view).result(binding)
49
+ puts result if output
50
+ result
51
+ end
52
+
53
+ private
54
+
55
+ ##
56
+ # Take the given data hash and converts the key/value pairs into instance variables.
57
+ #
58
+ # @param data [Hash] the data to convert into instance variables.
59
+
60
+ def to_ivars(data)
61
+ data.to_h.each do |key, value|
62
+ instance_variable_set("@#{key.to_s}", value)
63
+ end
64
+ end
65
+ end
66
+ end
data/lib/mousevc.rb ADDED
@@ -0,0 +1,49 @@
1
+ require_relative 'mousevc/version.rb'
2
+ require_relative 'mousevc/app.rb'
3
+
4
+ ##
5
+ # Mousevc is the top level module and namespace for the Mousevc framework.
6
+
7
+ module Mousevc
8
+
9
+ ##
10
+ # @todo Add link to documentation
11
+ #
12
+ # @return [String] some pretty ASCII art
13
+
14
+ def self.art
15
+ lines = [
16
+ "",
17
+ "(`) (`)",
18
+ "=('o')=",
19
+ " m m ",
20
+ "",
21
+ "MousevC",
22
+ "V L",
23
+ "C I",
24
+ "",
25
+ "by",
26
+ "Bideo Wego",
27
+ "http://bideowego.com/mousevc",
28
+ "",
29
+ "Documentation",
30
+ "http://www.rubydoc.info/gems/mousevc"
31
+ ]
32
+ width = lines.max.length
33
+ width = width.even? ? width + 1 : width
34
+ lines.map do |s|
35
+ s.center(width)
36
+ end.join("\n")
37
+ end
38
+
39
+ ##
40
+ # Generates a Mousevc class constant ready for instantiation
41
+ #
42
+ # @return [Constant] the class constant
43
+
44
+ def self.factory(class_name)
45
+ "Mousevc::#{class_name}".split('::').inject(Object) do |object, string|
46
+ object.const_get(string)
47
+ end
48
+ end
49
+ end
data/mousevc.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mousevc/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mousevc"
8
+ spec.version = Mousevc::VERSION
9
+ spec.authors = ["Chris Scavello"]
10
+ spec.email = ["bideowego@gmail.com"]
11
+
12
+ spec.summary = %q{A tiny mouse sized MVC framework to jump start command line apps.}
13
+ #spec.description = %q{TODO: Write a longer description or delete this line.}
14
+ spec.homepage = "http://bideowego.com/mousevc"
15
+ spec.license = "MIT"
16
+
17
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
18
+ # delete this section to allow pushing this gem to any host.
19
+ if spec.respond_to?(:metadata)
20
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
21
+ else
22
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
23
+ end
24
+
25
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_development_dependency "bundler", "~> 1.10"
31
+ spec.add_development_dependency "rake", "~> 10.0"
32
+ spec.add_development_dependency "rspec"
33
+ end