acts_as_wrapped_class 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +5 -0
- data/Manifest.txt +7 -0
- data/README.txt +63 -0
- data/Rakefile +17 -0
- data/lib/acts_as_wrapped_class.rb +144 -0
- data/lib/wrapper_base.rb +36 -0
- data/test/test_acts_as_wrapped_class.rb +128 -0
- metadata +63 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
data/README.txt
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
ActsAsWrappedClass
|
2
|
+
by David Stevenson
|
3
|
+
http://elctech.com/blog
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
ActsAsWrappedClass is designed to automatically generate a wrapper for an object that you don't want to be allowed to access certain methods in. This is useful in cases where you want to sandbox what users' code can and can't do, by providing them access to the wrapper classes rather than the original classes.
|
8
|
+
|
9
|
+
== FEATURES/PROBLEMS:
|
10
|
+
|
11
|
+
* Wrappers do not dispatch const_missing yet, so constants are not accessible yet.
|
12
|
+
|
13
|
+
== SYNOPSIS:
|
14
|
+
|
15
|
+
class Something
|
16
|
+
acts_as_wrapped_class :methods => [:safe_method]
|
17
|
+
# SomethingWrapper is now defined
|
18
|
+
|
19
|
+
def safe_method # allowed to access this method through SomethingWrapper
|
20
|
+
Something.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def unsafe_method # not allowed to access this method through SomethingWrapper
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
s = Something.new
|
28
|
+
wrapper = s.to_wrapper
|
29
|
+
wrapper.safe_method # returns a new SomethingWrapper
|
30
|
+
wrapper.unsafe_method # raises an exception
|
31
|
+
|
32
|
+
== REQUIREMENTS:
|
33
|
+
|
34
|
+
* none
|
35
|
+
|
36
|
+
== INSTALL:
|
37
|
+
|
38
|
+
* sudo gem install acts_as_wrapped_class
|
39
|
+
|
40
|
+
== LICENSE:
|
41
|
+
|
42
|
+
(The MIT License)
|
43
|
+
|
44
|
+
Copyright (c) 2007 David Stevenson
|
45
|
+
|
46
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
47
|
+
a copy of this software and associated documentation files (the
|
48
|
+
'Software'), to deal in the Software without restriction, including
|
49
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
50
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
51
|
+
permit persons to whom the Software is furnished to do so, subject to
|
52
|
+
the following conditions:
|
53
|
+
|
54
|
+
The above copyright notice and this permission notice shall be
|
55
|
+
included in all copies or substantial portions of the Software.
|
56
|
+
|
57
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
58
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
59
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
60
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
61
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
62
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
63
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
require './lib/acts_as_wrapped_class.rb'
|
6
|
+
|
7
|
+
Hoe.new('acts_as_wrapped_class', ActsAsWrappedClass::VERSION) do |p|
|
8
|
+
p.rubyforge_name = 'acts_as_wrapped_class'
|
9
|
+
p.author = 'David Stevenson'
|
10
|
+
p.email = 'ds@elctech.com'
|
11
|
+
p.summary = 'automatically generate wrapper classes which restrict access to methods and constants in the wrapped class'
|
12
|
+
p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
|
13
|
+
p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
|
14
|
+
p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
|
15
|
+
end
|
16
|
+
|
17
|
+
# vim: syntax=Ruby
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require "erb"
|
2
|
+
|
3
|
+
module ActsAsWrappedClass
|
4
|
+
VERSION = "1.0.0"
|
5
|
+
WRAPPED_CLASSES = []
|
6
|
+
|
7
|
+
module InstanceMethods
|
8
|
+
def to_wrapper
|
9
|
+
eval("#{self.class.name}Wrapper").new(self)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
def wrapped_class?
|
15
|
+
true
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class WrapperFinder
|
20
|
+
SANDBOX_BASE_IMPORTS = ["Object", "Module", "Class", "Kernel", "Main", "Array", "Bignum", "Binding", "Comparable", "Cont", "Data", "Dir", "Enumerable", "Exception", "FalseClass", "FConst", "File", "FileTest", "Fixnum", "Float", "GC", "Hash", "Integer", "IO", "Marshal", "Math", "Match", "Method", "NilClass", "Numeric", "ObSpace", "Precision", "Proc", "Process", "ProcStatus", "ProcUID", "ProcGID", "ProcID_Syscall", "Range", "Regexp", "Stat", "String", "Struct", "Symbol", "Thread", "ThGroup", "Time", "Tms", "TrueClass", "UnboundMethod", "StandardError", "SystemExit", "Interrupt", "Signal", "Fatal", "ArgError", "EOFError", "IndexError", "RangeError", "RegexpError", "IOError", "RuntimeError", "SecurityError", "SystemCallError", "SysStackError", "ThreadError", "TypeError", "ZeroDivError", "NotImpError", "NoMemError", "NoMethodError", "FloatDomainError", "ScriptError", "NameError", "NameErrorMesg", "SyntaxError", "LoadError", "LocalJumpError", "Errno", "BoxedClass"]
|
21
|
+
@@special_handlers = {Array => Proc.new{ |object| object.collect{|val| find_wrapper_for(val)} },
|
22
|
+
Hash => Proc.new{ |object| object.inject({}){|h, key_val| h[find_wrapper_for(key_val[0])] = find_wrapper_for(key_val[1]); h } }}
|
23
|
+
|
24
|
+
# Returns a wrapper for an instance of an object, if one exsits, or the original object if it's a core datatype.
|
25
|
+
# * This will first attempt to find a special handler for the type of object being wrapped and invoke it's block
|
26
|
+
# * Then it will look for a wrapper classes that fits the object's type (Something looks for SomethingWrapper)
|
27
|
+
# * Finally, it checks a list of "safe" classes that don't need wrapping
|
28
|
+
# If no match is found, an exception is raised
|
29
|
+
def self.find_wrapper_for(object)
|
30
|
+
wrapper_name = "#{object.class.name}Wrapper"
|
31
|
+
return nil if nil
|
32
|
+
|
33
|
+
@@special_handlers.each do |key, value|
|
34
|
+
return value.call(object) if object.is_a?(key)
|
35
|
+
end
|
36
|
+
|
37
|
+
return object if object.kind_of?(WrapperBase)
|
38
|
+
return eval(wrapper_name).new(object) if eval("defined?(#{wrapper_name})")
|
39
|
+
return object if SANDBOX_BASE_IMPORTS.include?(object.class.name)
|
40
|
+
|
41
|
+
raise "Can't find wrapper for class: #{object.class.name}"
|
42
|
+
end
|
43
|
+
|
44
|
+
# Add a special handler for how to wrap certain types of classes.
|
45
|
+
# For example, if you wanted to wrap Arrays by wrapping each of their elements
|
46
|
+
def self.add_special_handler(klass, prc)
|
47
|
+
raise "1st arg must be a Class" unless klass.is_a?(Class)
|
48
|
+
raise "2st arg must be a Proc" unless prc.is_a?(Proc)
|
49
|
+
@@special_handlers[klass] = prc
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def wrapped_class?
|
54
|
+
false
|
55
|
+
end
|
56
|
+
|
57
|
+
# Mark a class as wrapped, creating a wrapper class which allows access to certain methods specified by EITHER the :methods safe list of the :except_methods blacklist.
|
58
|
+
# You cannot use both :methods and :except_methods at once.
|
59
|
+
# * options[:methods] contains a list of method names (symbols) to allow access to
|
60
|
+
# * options[:except_methods] contains a list of method names (symbols) to not allow access to
|
61
|
+
# * options[:constants] contains a list of constant names (symbols) to allow access to
|
62
|
+
# * options[:except_constants] contains a list of constant names (symbols) to not allow access to
|
63
|
+
def acts_as_wrapped_class(options = {})
|
64
|
+
raise "Can't specify methods to allow and to deny." if options[:methods] && options[:except_methods]
|
65
|
+
raise "Can't specify constants to allow and to deny." if options[:constants] && options[:except_constants]
|
66
|
+
options[:methods] ||= :all
|
67
|
+
options[:constants] ||= :all
|
68
|
+
|
69
|
+
WRAPPED_CLASSES << self
|
70
|
+
|
71
|
+
if options[:methods] == :all
|
72
|
+
options.delete(:methods)
|
73
|
+
options[:except_methods] = []
|
74
|
+
end
|
75
|
+
|
76
|
+
if options[:constants] == :all
|
77
|
+
options.delete(:constants)
|
78
|
+
options[:except_constants] = []
|
79
|
+
end
|
80
|
+
|
81
|
+
meths = options[:methods] || options[:except_methods]
|
82
|
+
consts = options[:constants] || options[:except_constants]
|
83
|
+
|
84
|
+
allowed_method_missing = options[:methods] ? options[:methods].include?(:method_missing) : !options[:except_methods].include?(:method_missing)
|
85
|
+
allowed_const_missing = options[:constants] ? options[:constants].include?(:cont_missing) : !options[:except_constants].include?(:const_missing)
|
86
|
+
|
87
|
+
self.send(:include, ActsAsWrappedClass::InstanceMethods)
|
88
|
+
self.send(:extend, ActsAsWrappedClass::ClassMethods)
|
89
|
+
|
90
|
+
if allowed_method_missing
|
91
|
+
method_defs_erb = <<-EOF
|
92
|
+
def method_missing(meth, *args);
|
93
|
+
<% if options[:except_methods] %>
|
94
|
+
raise NameError.new("Method `"+meth.to_s+"' now allowed") if [<%= meths.collect {|m| ":\#{m}"}.join(", ") %>].include?(meth)
|
95
|
+
<% else %>
|
96
|
+
raise NameError.new("Method `"+meth.to_s+"' now allowed") unless [<%= meths.collect {|m| ":\#{m}"}.join(", ") %>].include?(meth) || !@wrapped_object.class.method_defined?(meth)
|
97
|
+
<% end %>
|
98
|
+
ActsAsWrappedClass::WrapperFinder.find_wrapper_for(@wrapped_object.send(meth, *args));
|
99
|
+
end
|
100
|
+
EOF
|
101
|
+
else
|
102
|
+
method_defs_erb = <<-EOF
|
103
|
+
def method_missing(meth, *args);
|
104
|
+
raise NameError.new("Method `"+meth.to_s+"' now allowed") <%= options[:methods] ? "unless" : "if"%> [<%=meths.collect {|m| ":\#{m}"}.join(", ")%>].include?(meth)
|
105
|
+
ActsAsWrappedClass::WrapperFinder.find_wrapper_for(@wrapped_object.method(meth).call(*args));
|
106
|
+
end
|
107
|
+
EOF
|
108
|
+
end
|
109
|
+
|
110
|
+
# if allowed_const_missing
|
111
|
+
# const_defs_erb = <<-EOF
|
112
|
+
# def const_missing(const, *args);
|
113
|
+
# <% if options[:except_constants] %>
|
114
|
+
# raise NameError.new("Constant `"+const.to_s+"' now allowed") if [<%= consts.collect {|c| ":\#{c}"}.join(", ") %>].include?(const)
|
115
|
+
# <% else %>
|
116
|
+
# raise NameError.new("Method `"+meth.to_s+"' now allowed") unless [<%= meths.collect {|c| ":\#{c}"}.join(", ") %>].include?(const) || !@wrapped_object.class.const_defined?(const)
|
117
|
+
# <% end %>
|
118
|
+
# @wrapped_object.const_get(const)
|
119
|
+
# end
|
120
|
+
# EOF
|
121
|
+
# else
|
122
|
+
# const_defs_erb = <<-EOF
|
123
|
+
# def const_missing(const, *args);
|
124
|
+
# raise NameError.new("Constant `"+const.to_s+"' now allowed") <%= options[:constants] ? "unless" : "if"%> [<%=consts.collect {|m| ":\#{m}"}.join(", ")%>].include?(const)
|
125
|
+
# @wrapped_object.const_get(const)
|
126
|
+
# end
|
127
|
+
# EOF
|
128
|
+
# end
|
129
|
+
|
130
|
+
method_defs = ERB.new(method_defs_erb).result(binding)
|
131
|
+
|
132
|
+
wrapper_class_code = <<-EOF
|
133
|
+
class #{self.name}Wrapper < WrapperBase
|
134
|
+
#{method_defs}
|
135
|
+
end
|
136
|
+
EOF
|
137
|
+
|
138
|
+
eval wrapper_class_code, TOPLEVEL_BINDING
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
Object.send(:include, ActsAsWrappedClass)
|
143
|
+
|
144
|
+
require File.join(File.dirname(__FILE__), "wrapper_base")
|
data/lib/wrapper_base.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
class WrapperBase
|
2
|
+
eval((public_instance_methods - ["__id__","__send__", "is_a?", "kind_of?", "hash", "class", "inspect"]).collect{|meth| "undef "+meth}.join("; "))
|
3
|
+
|
4
|
+
# Create a wrapper, passing in an object to wrap
|
5
|
+
def initialize(wrapped_object)
|
6
|
+
@wrapped_object = wrapped_object
|
7
|
+
end
|
8
|
+
|
9
|
+
def hash
|
10
|
+
@wrapped_object.hash
|
11
|
+
end
|
12
|
+
|
13
|
+
def ==(other)
|
14
|
+
return false if self.class != other.class
|
15
|
+
@wrapped_object == other._wrapped_object
|
16
|
+
end
|
17
|
+
|
18
|
+
def <=>(other)
|
19
|
+
raise "Can't compare objects of different types" if self.class != other.class
|
20
|
+
@wrapped_object <=> other._wrapped_object
|
21
|
+
end
|
22
|
+
|
23
|
+
def ===(other)
|
24
|
+
return false if self.class != other.class
|
25
|
+
@wrapped_object === other._wrapped_object
|
26
|
+
end
|
27
|
+
|
28
|
+
# Provide access to the wrapped object
|
29
|
+
def _wrapped_object
|
30
|
+
@wrapped_object
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.wrapper_class?
|
34
|
+
true
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require File.dirname(__FILE__) + "/../lib/acts_as_wrapped_class"
|
3
|
+
|
4
|
+
class NotWrappedClass
|
5
|
+
def method1
|
6
|
+
6.383
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class SampleClass #SampleClassWrapper
|
11
|
+
acts_as_wrapped_class :methods => [:get_other_class, :other_method], :constants => [:API]
|
12
|
+
|
13
|
+
API = 3.1415
|
14
|
+
|
15
|
+
def get_other_class
|
16
|
+
OtherClass.new(rand)
|
17
|
+
end
|
18
|
+
|
19
|
+
def other_method
|
20
|
+
5.5
|
21
|
+
end
|
22
|
+
|
23
|
+
def unsafe_method
|
24
|
+
666
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class OtherClass
|
29
|
+
SAMPLE_CLASS = SampleClass.new
|
30
|
+
|
31
|
+
acts_as_wrapped_class :methods => :all, :constants => :all
|
32
|
+
|
33
|
+
attr_reader :value
|
34
|
+
|
35
|
+
def initialize(val)
|
36
|
+
@value = val
|
37
|
+
end
|
38
|
+
|
39
|
+
def get_sample_classes
|
40
|
+
[SampleClass.new] * 10
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_hash
|
44
|
+
{:a => SampleClass.new, OtherClass.new(11) => "hello", :array => [SampleClass.new, SampleClass.new, 10]}
|
45
|
+
end
|
46
|
+
|
47
|
+
def another_method
|
48
|
+
6.6
|
49
|
+
end
|
50
|
+
|
51
|
+
def hash
|
52
|
+
@value
|
53
|
+
end
|
54
|
+
|
55
|
+
def ==(other)
|
56
|
+
other.value == value
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class ActsAsWrappedCodeTest < Test::Unit::TestCase
|
61
|
+
# Replace this with your real tests.
|
62
|
+
def test_wrappers_exist
|
63
|
+
assert defined?(OtherClassWrapper)
|
64
|
+
assert defined?(SampleClassWrapper)
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_unwrappers_to_wrapper
|
68
|
+
assert SampleClass.public_instance_methods.include?("to_wrapper")
|
69
|
+
assert OtherClass.public_instance_methods.include?("to_wrapper")
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_awrappers_clean
|
73
|
+
assert_contents_same ["method_missing"] + allowed_methods, SampleClassWrapper.public_instance_methods
|
74
|
+
assert_contents_same ["method_missing"] + allowed_methods, OtherClassWrapper.public_instance_methods
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_wrappers_method_missing_clean
|
78
|
+
wrap = SampleClass.new.to_wrapper
|
79
|
+
wrap.other_method
|
80
|
+
assert_raise(NameError) { wrap.unsafe_method }
|
81
|
+
assert_raise(NameError) { wrap.method_missing :unsafe_method }
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_class1_wrappers
|
85
|
+
wrap = SampleClass.new.to_wrapper
|
86
|
+
assert wrap.is_a?(SampleClassWrapper)
|
87
|
+
assert wrap.other_method.is_a?(Float)
|
88
|
+
assert_equal 5.5, wrap.other_method
|
89
|
+
assert wrap.get_other_class.is_a?(OtherClassWrapper)
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_hash_and_equals
|
93
|
+
wrap1 = OtherClass.new(11).to_wrapper
|
94
|
+
wrap2 = OtherClass.new(11).to_wrapper
|
95
|
+
assert_equal wrap1.hash, wrap2.hash
|
96
|
+
assert_equal wrap1, wrap2
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
def test_class2_wrappers
|
101
|
+
wrap = OtherClass.new(10.0).to_wrapper
|
102
|
+
assert wrap.is_a?(OtherClassWrapper)
|
103
|
+
assert_equal 10.0, wrap.value
|
104
|
+
assert_equal 6.6, wrap.another_method
|
105
|
+
array = wrap.get_sample_classes
|
106
|
+
array.each do |a|
|
107
|
+
assert a.is_a?(SampleClassWrapper)
|
108
|
+
end
|
109
|
+
hash = wrap.get_hash
|
110
|
+
assert hash[:a].is_a?(SampleClassWrapper)
|
111
|
+
assert_contents_same hash[:array].collect{|v| v.class.name}, ["SampleClassWrapper", "SampleClassWrapper", "Fixnum"]
|
112
|
+
end
|
113
|
+
|
114
|
+
def test_wrapped_class?
|
115
|
+
assert SampleClass.wrapped_class?
|
116
|
+
assert OtherClass.wrapped_class?
|
117
|
+
assert !NotWrappedClass.wrapped_class?
|
118
|
+
end
|
119
|
+
|
120
|
+
def assert_contents_same(array1, array2)
|
121
|
+
assert_equal array1.length, array2.length, "#{array1.inspect} != #{array2.inspect}"
|
122
|
+
array1.each { |a| assert array2.include?(a), "#{array2.inspect} does not contain #{a.inspect}" }
|
123
|
+
end
|
124
|
+
|
125
|
+
def allowed_methods
|
126
|
+
["__id__", "__send__", "is_a?", "kind_of?", "class", "hash", "inspect", "==", "<=>", "===", "_wrapped_object"]
|
127
|
+
end
|
128
|
+
end
|
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.4
|
3
|
+
specification_version: 1
|
4
|
+
name: acts_as_wrapped_class
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 1.0.0
|
7
|
+
date: 2007-10-22 00:00:00 -07:00
|
8
|
+
summary: automatically generate wrapper classes which restrict access to methods and constants in the wrapped class
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: ds@elctech.com
|
12
|
+
homepage: " by David Stevenson "
|
13
|
+
rubyforge_project: acts_as_wrapped_class
|
14
|
+
description: "== FEATURES/PROBLEMS: * Wrappers do not dispatch const_missing yet, so constants are not accessible yet. == SYNOPSIS: class Something acts_as_wrapped_class :methods => [:safe_method] # SomethingWrapper is now defined def safe_method # allowed to access this method through SomethingWrapper Something.new end def unsafe_method # not allowed to access this method through SomethingWrapper end end"
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- David Stevenson
|
31
|
+
files:
|
32
|
+
- History.txt
|
33
|
+
- Manifest.txt
|
34
|
+
- README.txt
|
35
|
+
- Rakefile
|
36
|
+
- lib/acts_as_wrapped_class.rb
|
37
|
+
- lib/wrapper_base.rb
|
38
|
+
- test/test_acts_as_wrapped_class.rb
|
39
|
+
test_files:
|
40
|
+
- test/test_acts_as_wrapped_class.rb
|
41
|
+
rdoc_options:
|
42
|
+
- --main
|
43
|
+
- README.txt
|
44
|
+
extra_rdoc_files:
|
45
|
+
- History.txt
|
46
|
+
- Manifest.txt
|
47
|
+
- README.txt
|
48
|
+
executables: []
|
49
|
+
|
50
|
+
extensions: []
|
51
|
+
|
52
|
+
requirements: []
|
53
|
+
|
54
|
+
dependencies:
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: hoe
|
57
|
+
version_requirement:
|
58
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 1.3.0
|
63
|
+
version:
|