stickler 2.3.0 → 2.4.0
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.
- checksums.yaml +15 -0
- data/CONTRIBUTING.md +5 -4
- data/HISTORY.md +16 -9
- data/LICENSE +1 -1
- data/Manifest.txt +34 -20
- data/README.md +128 -0
- data/Rakefile +10 -9
- data/bin/stickler +9 -6
- data/bin/stickler-passenger-config +8 -8
- data/bin/stickler-server +12 -12
- data/examples/as_middleware.ru +14 -0
- data/examples/auth_repo.ru +1 -1
- data/examples/gemcutter_repo.ru +1 -1
- data/examples/local_repo.ru +1 -1
- data/lib/stickler.rb +3 -1
- data/lib/stickler/client.rb +2 -1
- data/lib/stickler/client/delete.rb +1 -1
- data/lib/stickler/client/latest-version.rb +40 -0
- data/lib/stickler/client/mirror.rb +47 -15
- data/lib/stickler/client/push.rb +1 -1
- data/lib/stickler/client/unyank.rb +1 -1
- data/lib/stickler/client/yank.rb +1 -1
- data/lib/stickler/gem_container.rb +40 -0
- data/lib/stickler/gemfile_lock_parser.rb +47 -0
- data/lib/stickler/middleware.rb +1 -0
- data/lib/stickler/middleware/server.rb +37 -0
- data/lib/stickler/repository/api.rb +16 -0
- data/lib/stickler/repository/index.rb +0 -3
- data/lib/stickler/repository/local.rb +6 -8
- data/lib/stickler/repository/remote.rb +29 -7
- data/lib/stickler/server.rb +2 -6
- data/man/stickler-passenger-config.1 +2 -22
- data/man/stickler-server.1 +9 -99
- data/man/stickler.1 +15 -173
- data/man/stickler.1.ronn +6 -0
- data/tasks/default.rake +16 -18
- data/tasks/man.rake +7 -0
- data/tasks/this.rb +5 -5
- data/test/data/Gemfile.lock.example +56 -0
- data/{spec → test}/data/gemcutter/gems/foo-1.0.0.gem +0 -0
- data/{spec → test}/data/gems/bar-1.0.0.gem +0 -0
- data/{spec → test}/data/gems/baz-3.1.4-java.gem +0 -0
- data/{spec → test}/data/gems/baz-3.1.4.gem +0 -0
- data/{spec → test}/data/gems/foo-1.0.0.gem +0 -0
- data/{spec → test}/data/gems/foo-2.0.0a.gem +0 -0
- data/test/data/specifications/bar-1.0.0.gemspec +31 -0
- data/test/data/specifications/baz-3.1.4-java.gemspec +32 -0
- data/test/data/specifications/baz-3.1.4.gemspec +31 -0
- data/test/data/specifications/foo-1.0.0.gemspec +31 -0
- data/test/data/specifications/foo-2.0.0a.gemspec +32 -0
- data/test/index_test_helpers.rb +75 -0
- data/test/middleware/test_local.rb +75 -0
- data/test/middleware/test_not_found.rb +26 -0
- data/test/repository/test_api.rb +49 -0
- data/test/repository/test_api_behavior.rb +208 -0
- data/test/repository/test_index.rb +48 -0
- data/test/repository/test_local.rb +59 -0
- data/test/repository/test_null.rb +15 -0
- data/test/repository/test_remote.rb +26 -0
- data/test/repository/test_remote_authenticated.rb +39 -0
- data/test/stickler_test_server.rb +35 -0
- data/test/test_gemfile_lock_parser.rb +28 -0
- data/test/test_paths.rb +22 -0
- data/test/test_spec_lite.rb +90 -0
- data/test/test_stickler.rb +49 -0
- metadata +58 -85
- data/README.rdoc +0 -156
- data/spec/index_spec_helpers.rb +0 -73
- data/spec/middleware/local_spec.rb +0 -72
- data/spec/middleware/not_found_spec.rb +0 -25
- data/spec/paths_spec.rb +0 -11
- data/spec/repository/api_behavior.rb +0 -192
- data/spec/repository/api_spec.rb +0 -37
- data/spec/repository/index_spec.rb +0 -46
- data/spec/repository/local_spec.rb +0 -49
- data/spec/repository/null_spec.rb +0 -14
- data/spec/repository/remote_spec.rb +0 -86
- data/spec/spec.opts +0 -2
- data/spec/spec_helper.rb +0 -24
- data/spec/spec_lite_spec.rb +0 -96
@@ -0,0 +1,14 @@
|
|
1
|
+
#-----------------------------------------------------------------------
|
2
|
+
#-*- vim: set ft=ruby: -*-
|
3
|
+
#
|
4
|
+
# Example rackup file for using stickler as middleware in a larger application
|
5
|
+
#-----------------------------------------------------------------------
|
6
|
+
$:.unshift File.expand_path( File.join( File.dirname(__FILE__), "..", "lib" ) )
|
7
|
+
|
8
|
+
require 'stickler'
|
9
|
+
|
10
|
+
root = File.expand_path( File.join( File.dirname( __FILE__ ), *%w[ .. test data ]))
|
11
|
+
|
12
|
+
puts root
|
13
|
+
use ::Stickler::Middleware::Server, :stickler_root => root
|
14
|
+
run ::Sinatra::Base
|
data/examples/auth_repo.ru
CHANGED
@@ -7,7 +7,7 @@ $:.unshift File.expand_path( File.join( File.dirname(__FILE__), "..", "lib" ) )
|
|
7
7
|
|
8
8
|
require 'stickler'
|
9
9
|
|
10
|
-
tmp = File.expand_path( File.join( File.dirname( __FILE__ ), "..", "
|
10
|
+
tmp = File.expand_path( File.join( File.dirname( __FILE__ ), "..", "test", "tmp" ) )
|
11
11
|
|
12
12
|
use Rack::Auth::Basic, 'Secure Stickler' do |u,p|
|
13
13
|
(u == "stickler") and (p == "secret")
|
data/examples/gemcutter_repo.ru
CHANGED
@@ -12,7 +12,7 @@ $:.unshift File.expand_path( File.join( File.dirname(__FILE__), "..", "lib" ) )
|
|
12
12
|
|
13
13
|
require 'stickler'
|
14
14
|
|
15
|
-
gem_dir = File.expand_path( "../
|
15
|
+
gem_dir = File.expand_path( "../test/tmp", File.dirname( __FILE__ ) )
|
16
16
|
|
17
17
|
use ::Stickler::Middleware::Compression
|
18
18
|
use ::Stickler::Middleware::Gemcutter, :repo_root => gem_dir
|
data/examples/local_repo.ru
CHANGED
@@ -9,7 +9,7 @@ $:.unshift File.expand_path( File.join( File.dirname(__FILE__), "..", "lib" ) )
|
|
9
9
|
require 'stickler/middleware/compression'
|
10
10
|
require 'stickler/middleware/local'
|
11
11
|
|
12
|
-
gem_dir = File.expand_path( File.join( File.dirname( __FILE__ ), *%w[ ..
|
12
|
+
gem_dir = File.expand_path( File.join( File.dirname( __FILE__ ), *%w[ .. test data ]))
|
13
13
|
|
14
14
|
puts gem_dir
|
15
15
|
use ::Stickler::Middleware::Compression
|
data/lib/stickler.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module Stickler
|
2
2
|
# The Current Version of the library
|
3
|
-
VERSION = "2.
|
3
|
+
VERSION = "2.4.0"
|
4
4
|
end
|
5
5
|
require 'sinatra/base'
|
6
6
|
|
@@ -8,6 +8,8 @@ require 'stickler/logable'
|
|
8
8
|
require 'stickler/error'
|
9
9
|
require 'stickler/paths'
|
10
10
|
require 'stickler/spec_lite'
|
11
|
+
require 'stickler/gem_container'
|
12
|
+
require 'stickler/gemfile_lock_parser'
|
11
13
|
|
12
14
|
require 'stickler/repository'
|
13
15
|
require 'stickler/middleware'
|
data/lib/stickler/client.rb
CHANGED
@@ -30,7 +30,7 @@ module Stickler
|
|
30
30
|
def parse( argv )
|
31
31
|
opts = Trollop::with_standard_exception_handling( parser ) do
|
32
32
|
o = parser.parse( argv )
|
33
|
-
yield parser if block_given?
|
33
|
+
yield( parser, o ) if block_given?
|
34
34
|
return o
|
35
35
|
end
|
36
36
|
return opts
|
@@ -49,3 +49,4 @@ require 'stickler/client/mirror'
|
|
49
49
|
require 'stickler/client/push'
|
50
50
|
require 'stickler/client/unyank'
|
51
51
|
require 'stickler/client/yank'
|
52
|
+
require 'stickler/client/latest-version'
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Stickler
|
2
|
+
class Client
|
3
|
+
class LatestVersion < Stickler::Client
|
4
|
+
def self.banner
|
5
|
+
<<-_
|
6
|
+
Prints the latest version of a gem
|
7
|
+
|
8
|
+
Usage: stickler latest-version gem-name
|
9
|
+
|
10
|
+
Options:
|
11
|
+
_
|
12
|
+
end
|
13
|
+
|
14
|
+
def parse( argv )
|
15
|
+
gem_name = nil
|
16
|
+
opts = super( argv ) do |p,o|
|
17
|
+
raise Trollop::CommandlineError, "At lest one gem-name is required" if p.leftovers.empty?
|
18
|
+
gem_name = p.leftovers.shift
|
19
|
+
end
|
20
|
+
opts[:gem_name] = gem_name
|
21
|
+
return opts
|
22
|
+
end
|
23
|
+
|
24
|
+
def run
|
25
|
+
opts = parse( self.argv )
|
26
|
+
repo = remote_repo_for( opts )
|
27
|
+
match = repo.latest_specs_list.find do |name, version, platform|
|
28
|
+
name == opts[:gem_name]
|
29
|
+
end
|
30
|
+
if match then
|
31
|
+
$stdout.puts match[1]
|
32
|
+
else
|
33
|
+
$stdout.puts "Gem #{opts[:gem_name]} not found in remote repository"
|
34
|
+
end
|
35
|
+
rescue Stickler::Repository::Error => e
|
36
|
+
$stdout.puts "ERROR: #{e.message}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -5,9 +5,11 @@ module Stickler
|
|
5
5
|
def self.banner
|
6
6
|
<<-_
|
7
7
|
Pull a specific version of a gem from an upstream gem server
|
8
|
-
and store it in a stickler server.
|
8
|
+
and store it in a stickler server. Either a specific version
|
9
|
+
must be specificied, or a Gemfile.lock must be used.
|
9
10
|
|
10
11
|
Usage: stickler mirror [options] --gem-version x.y.z gem
|
12
|
+
stickler mirror [options] Gemfile.lock
|
11
13
|
|
12
14
|
Options:
|
13
15
|
_
|
@@ -17,19 +19,26 @@ _
|
|
17
19
|
unless @parser then
|
18
20
|
@parser = super
|
19
21
|
@parser.opt( :upstream, "The upstream gem server from which to pull", :type => :string, :default => Client.config.upstream )
|
20
|
-
@parser.opt( :gem_version, "The version of the gem to
|
21
|
-
@parser.opt( :platform, "The platform of the gem to
|
22
|
+
@parser.opt( :gem_version, "The version of the gem to mirror", :type => :string)
|
23
|
+
@parser.opt( :platform, "The platform of the gem to mirror", :type => :string, :default => ::Gem::Platform::RUBY )
|
22
24
|
end
|
23
25
|
return @parser
|
24
26
|
end
|
25
27
|
|
26
28
|
def parse( argv )
|
27
|
-
gem_name
|
28
|
-
|
29
|
-
|
30
|
-
|
29
|
+
gem_name = nil
|
30
|
+
gemfile_lock = nil
|
31
|
+
opts = super( argv ) do |p, o|
|
32
|
+
raise Trollop::CommandlineError, "A Gemfile.lock or a gem name is required to mirror" if p.leftovers.empty?
|
33
|
+
if o[:gem_version] then
|
34
|
+
gem_name = p.leftovers.shift
|
35
|
+
else
|
36
|
+
gemfile_lock = p.leftovers.shift
|
37
|
+
raise Trollop::CommandlineError, "#{lock} must be readable" unless File.readable?( gemfile_lock )
|
38
|
+
end
|
31
39
|
end
|
32
|
-
opts[:gem_name]
|
40
|
+
opts[:gem_name] = gem_name
|
41
|
+
opts[:gemfile_lock] = gemfile_lock
|
33
42
|
return opts
|
34
43
|
end
|
35
44
|
|
@@ -41,24 +50,47 @@ _
|
|
41
50
|
Stickler::Repository::RemoteMirror.new( opts[:server], :debug => opts[:debug] )
|
42
51
|
end
|
43
52
|
|
44
|
-
def
|
45
|
-
opts
|
46
|
-
|
47
|
-
|
48
|
-
|
53
|
+
def spec_list( opts )
|
54
|
+
if opts[:gem_name] then
|
55
|
+
return [Stickler::SpecLite.new( opts[:gem_name], opts[:gem_version], opts[:platform] )]
|
56
|
+
end
|
57
|
+
|
58
|
+
if opts[:gemfile_lock] then
|
59
|
+
parser = Stickler::GemfileLockParser.new( opts[:gemfile_lock] )
|
60
|
+
return parser.gem_dependencies
|
61
|
+
end
|
62
|
+
raise Sticker::Error, "No gem name, or gemfile lock... no idea what to do"
|
63
|
+
end
|
49
64
|
|
65
|
+
def mirror_one_spec( repo, spec, upstream_host )
|
50
66
|
$stdout.write "Asking #{repo.uri} to mirror #{spec.full_name} from #{upstream_host} : "
|
51
67
|
$stdout.flush
|
52
68
|
|
53
69
|
resp = repo.mirror( spec, upstream_host )
|
54
|
-
|
55
70
|
$stdout.puts "OK -> #{repo.uri.join(resp.headers['Location'])}"
|
71
|
+
|
72
|
+
rescue Stickler::Repository::Error => e
|
73
|
+
$stdout.puts "ERROR: #{e.message}"
|
74
|
+
rescue StandardError => e
|
75
|
+
$stdout.puts e.backtrace.join("\n")
|
76
|
+
$stdout.puts "ERROR -> #{e.message}"
|
77
|
+
end
|
78
|
+
|
79
|
+
def run
|
80
|
+
opts = parse( self.argv )
|
81
|
+
repo = remote_repo_for( opts )
|
82
|
+
specs = spec_list( opts )
|
83
|
+
upstream_host = Addressable::URI.parse( opts[:upstream] ).host
|
84
|
+
|
85
|
+
specs.each do |spec|
|
86
|
+
mirror_one_spec( repo, spec, upstream_host )
|
87
|
+
end
|
56
88
|
rescue Stickler::Repository::Error => e
|
57
89
|
$stdout.puts "ERROR: #{e.message}"
|
58
90
|
rescue StandardError => e
|
59
91
|
puts e.backtrace.join("\n")
|
60
92
|
$stdout.puts "ERROR -> #{e.message}"
|
61
|
-
|
93
|
+
end
|
62
94
|
end
|
63
95
|
end
|
64
96
|
end
|
data/lib/stickler/client/push.rb
CHANGED
@@ -14,7 +14,7 @@ _
|
|
14
14
|
|
15
15
|
def parse( argv )
|
16
16
|
gemfiles = []
|
17
|
-
opts = super do |p|
|
17
|
+
opts = super do |p,o|
|
18
18
|
raise Trollop::CommandlineError, "At least one file is required to push" if p.leftovers.empty?
|
19
19
|
p.leftovers.each do |gemfile|
|
20
20
|
raise Trollop::CommandlineError, "#{gemfile} must be readable" unless File.readable?( gemfile )
|
data/lib/stickler/client/yank.rb
CHANGED
@@ -0,0 +1,40 @@
|
|
1
|
+
module Stickler
|
2
|
+
#
|
3
|
+
# Wrap the class that opens the gem file and gives access to all the gem file
|
4
|
+
# internals. The class that implements this in rubygems itself changed, so we
|
5
|
+
# need to be backwards compatible with folks that are using older versions of
|
6
|
+
# rubygems.
|
7
|
+
#
|
8
|
+
class GemContainer
|
9
|
+
attr_reader :path
|
10
|
+
def initialize( gem_file_path )
|
11
|
+
@path = gem_file_path
|
12
|
+
@container = load_container( path )
|
13
|
+
end
|
14
|
+
|
15
|
+
def spec
|
16
|
+
@container.spec
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# Rubygems transitions to using Gem::Package, so if we have that use it,
|
22
|
+
# otherwise fall back to the older method of using Gem::Format
|
23
|
+
begin
|
24
|
+
require 'rubygems/package'
|
25
|
+
def load_container( path )
|
26
|
+
Gem::Package.new( path )
|
27
|
+
end
|
28
|
+
rescue LoadError
|
29
|
+
puts "Unable to load 'rubygems/package' falling back to Gem::Format"
|
30
|
+
begin
|
31
|
+
require 'rubygems/format'
|
32
|
+
def load_container( path )
|
33
|
+
Gem::Format.from_file_by_path( path )
|
34
|
+
end
|
35
|
+
rescue LoadError
|
36
|
+
abort "FAilure to load rubygems/format"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Stickler
|
2
|
+
class GemfileLockParser
|
3
|
+
attr_reader :gem_dependencies
|
4
|
+
|
5
|
+
|
6
|
+
def initialize( path )
|
7
|
+
p = Pathname.new( path )
|
8
|
+
raise Stickler::Error, "#{path} does not exist" unless p.exist?
|
9
|
+
raise Stickler::Error, "#{path} is not readable" unless p.readable?
|
10
|
+
parse( p.read )
|
11
|
+
end
|
12
|
+
|
13
|
+
def depends_on?( name )
|
14
|
+
gem_dependencies.any?{ |spec| spec.name == name }
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def parse( text )
|
20
|
+
parts = partition( text )
|
21
|
+
@gem_dependencies = parse_dependencies( parts['GEM'] )
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse_dependencies( lines )
|
25
|
+
drop_until_specs( lines )
|
26
|
+
deps = []
|
27
|
+
lines.each do |line|
|
28
|
+
md = line.match( /\A\s{4}(\S+)\s+\(([\w\.]+)\)\Z/ )
|
29
|
+
next if md.nil?
|
30
|
+
deps << Stickler::SpecLite.new( md.captures[0], md.captures[1] )
|
31
|
+
end
|
32
|
+
return deps
|
33
|
+
end
|
34
|
+
|
35
|
+
def drop_until_specs( lines )
|
36
|
+
lines.drop_while{ |l| %w[ remote specs ].include?( l.strip.split(":").first ) }
|
37
|
+
end
|
38
|
+
|
39
|
+
def partition( text )
|
40
|
+
text.split("\n\n").each_with_object({}) { | p, h |
|
41
|
+
next if p.empty?
|
42
|
+
parts = p.split("\n").map(&:rstrip)
|
43
|
+
h[parts.first] = parts[1..-1]
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/stickler/middleware.rb
CHANGED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
module Stickler::Middleware
|
3
|
+
# Server is the entire stickler stack as a single piece of middleware that
|
4
|
+
# may be used by other libraries that would like to include Stickler in their
|
5
|
+
# application.
|
6
|
+
class Server
|
7
|
+
attr_reader :stickler_root
|
8
|
+
|
9
|
+
def initialize( app, opts = {} )
|
10
|
+
@app = app
|
11
|
+
@stickler_root = Pathname.new( opts.fetch( :stickler_root ) ).realpath
|
12
|
+
@run = server_app
|
13
|
+
validate
|
14
|
+
end
|
15
|
+
|
16
|
+
def call( env )
|
17
|
+
@run.call( env )
|
18
|
+
end
|
19
|
+
|
20
|
+
def server_app
|
21
|
+
root = self.stickler_root
|
22
|
+
Rack::Builder.app( @app )do
|
23
|
+
use Stickler::Middleware::Compression
|
24
|
+
use Stickler::Middleware::Gemcutter, :serve_indexes => false, :repo_root => root.join( "gemcutter" )
|
25
|
+
use Stickler::Middleware::Mirror, :serve_indexes => false, :repo_root => root.join( "mirror" )
|
26
|
+
use Stickler::Middleware::Index, :serve_indexes => true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def validate
|
33
|
+
raise ::Stickler::Error, "Stickler root directory '#{stickler_root}' must already exist" unless stickler_root.directory?
|
34
|
+
raise ::Stickler::Error, "Stickler root directory '#{stickler_root}' must be writable" unless stickler_root.writable?
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -109,6 +109,22 @@ module Stickler::Repository
|
|
109
109
|
raise NotImplementedError, not_implemented_msg( :yank )
|
110
110
|
end
|
111
111
|
|
112
|
+
#
|
113
|
+
# :call-seq:
|
114
|
+
# repo.unyank( spec ) -> true or nil
|
115
|
+
#
|
116
|
+
# "unyank" in the sense of undoing "yank"
|
117
|
+
#
|
118
|
+
# This means, put the gem matching +spec+ back into the index so that it
|
119
|
+
# will be found during searching.
|
120
|
+
#
|
121
|
+
# If the gem is sucessfully put back into the index then true is returned.
|
122
|
+
# Otherwise nil is returned
|
123
|
+
#
|
124
|
+
def unyank( spec )
|
125
|
+
raise NotImplementedError, not_implemented_msg( :unyank )
|
126
|
+
end
|
127
|
+
|
112
128
|
#
|
113
129
|
# :call-seq:
|
114
130
|
# repo.get( spec ) -> bytes
|
@@ -2,7 +2,6 @@ require 'stickler/repository/index'
|
|
2
2
|
require 'addressable/uri'
|
3
3
|
require 'tempfile'
|
4
4
|
require 'forwardable'
|
5
|
-
require 'rubygems/format'
|
6
5
|
|
7
6
|
module Stickler::Repository
|
8
7
|
#
|
@@ -180,13 +179,14 @@ module Stickler::Repository
|
|
180
179
|
#
|
181
180
|
def add( io )
|
182
181
|
# spooling to a temp file because Gem::Format.from_io() closes the io
|
183
|
-
# stream it is sent. Why it does this, I do not know.
|
182
|
+
# stream it is sent. Why it does this, I do not know. This may be
|
183
|
+
# removed once we are no longer using older rubygems
|
184
184
|
tempfile = Tempfile.new( "uploaded-gem.", temp_dir )
|
185
185
|
tempfile.write( io.read )
|
186
186
|
tempfile.rewind
|
187
187
|
|
188
|
-
|
189
|
-
spec = Stickler::SpecLite.new(
|
188
|
+
container = Stickler::GemContainer.new( tempfile.path )
|
189
|
+
spec = Stickler::SpecLite.new( container.spec.name, container.spec.version, container.spec.platform )
|
190
190
|
specs = search_for( spec )
|
191
191
|
|
192
192
|
raise Error, "gem #{spec.full_name} already exists" unless specs.empty?
|
@@ -201,8 +201,6 @@ module Stickler::Repository
|
|
201
201
|
# See Api#push
|
202
202
|
#
|
203
203
|
def push( path )
|
204
|
-
# is this line needed? Never used.
|
205
|
-
# spec = specification_from_gem_file( path )
|
206
204
|
result = nil
|
207
205
|
File.open( path ) do |io|
|
208
206
|
result = add( io )
|
@@ -302,8 +300,8 @@ module Stickler::Repository
|
|
302
300
|
end
|
303
301
|
|
304
302
|
def specification_from_gem_file( path )
|
305
|
-
|
306
|
-
return
|
303
|
+
container = Stickler::GemContainer.new( path )
|
304
|
+
return container.spec
|
307
305
|
end
|
308
306
|
|
309
307
|
def speclite_from_specification( spec )
|