indexer 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.index +54 -0
- data/HISTORY.md +9 -0
- data/README.md +145 -0
- data/bin/index +7 -0
- data/data/indexer/r2013/index.kwalify +175 -0
- data/data/indexer/r2013/index.yes +172 -0
- data/data/indexer/r2013/index.yesi +67 -0
- data/data/indexer/r2013/ruby.txt +35 -0
- data/data/indexer/r2013/yaml.txt +30 -0
- data/lib/indexer.rb +65 -0
- data/lib/indexer/attributes.rb +171 -0
- data/lib/indexer/command.rb +260 -0
- data/lib/indexer/components.rb +8 -0
- data/lib/indexer/components/author.rb +140 -0
- data/lib/indexer/components/conflict.rb +78 -0
- data/lib/indexer/components/copyright.rb +95 -0
- data/lib/indexer/components/dependency.rb +18 -0
- data/lib/indexer/components/organization.rb +133 -0
- data/lib/indexer/components/repository.rb +140 -0
- data/lib/indexer/components/requirement.rb +360 -0
- data/lib/indexer/components/resource.rb +209 -0
- data/lib/indexer/conversion.rb +14 -0
- data/lib/indexer/conversion/gemfile.rb +44 -0
- data/lib/indexer/conversion/gemspec.rb +114 -0
- data/lib/indexer/conversion/gemspec_exporter.rb +304 -0
- data/lib/indexer/core_ext.rb +4 -0
- data/lib/indexer/error.rb +23 -0
- data/lib/indexer/gemfile.rb +75 -0
- data/lib/indexer/importer.rb +144 -0
- data/lib/indexer/importer/file.rb +94 -0
- data/lib/indexer/importer/gemfile.rb +27 -0
- data/lib/indexer/importer/gemspec.rb +43 -0
- data/lib/indexer/importer/html.rb +289 -0
- data/lib/indexer/importer/markdown.rb +45 -0
- data/lib/indexer/importer/ruby.rb +47 -0
- data/lib/indexer/importer/version.rb +38 -0
- data/lib/indexer/importer/yaml.rb +46 -0
- data/lib/indexer/loadable.rb +159 -0
- data/lib/indexer/metadata.rb +879 -0
- data/lib/indexer/model.rb +237 -0
- data/lib/indexer/revision.rb +43 -0
- data/lib/indexer/valid.rb +287 -0
- data/lib/indexer/validator.rb +313 -0
- data/lib/indexer/version/constraint.rb +124 -0
- data/lib/indexer/version/exceptions.rb +11 -0
- data/lib/indexer/version/number.rb +497 -0
- metadata +141 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
module Indexer
|
2
|
+
|
3
|
+
class Importer
|
4
|
+
|
5
|
+
# Import metadata from a markdown source.
|
6
|
+
#
|
7
|
+
module MarkdownImportation
|
8
|
+
|
9
|
+
#
|
10
|
+
# Markdown import procedure.
|
11
|
+
#
|
12
|
+
def import(source)
|
13
|
+
if File.file?(source)
|
14
|
+
case File.extname(source)
|
15
|
+
when '.md', '.markdown'
|
16
|
+
load_markdown(source)
|
17
|
+
return true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
super(source) if defined?(super)
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# Import metadata from HTML file.
|
25
|
+
#
|
26
|
+
def load_markdown(file)
|
27
|
+
require 'nokogiri'
|
28
|
+
require 'redcarpet'
|
29
|
+
|
30
|
+
renderer = Redcarpet::Render::HTML.new()
|
31
|
+
markdown = Redcarpet::Markdown.new(renderer, :autolink=>true, :tables=>true, :space_after_headers=>true)
|
32
|
+
html = markdown.render(File.read(file))
|
33
|
+
doc = Nokogiri::HTML(html)
|
34
|
+
|
35
|
+
load_html(doc)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
# Include mixin into Importer class.
|
41
|
+
include MarkdownImportation
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Indexer
|
2
|
+
|
3
|
+
class Importer
|
4
|
+
|
5
|
+
# Build metadata from a Ruby script.
|
6
|
+
#
|
7
|
+
module RubyImportation
|
8
|
+
|
9
|
+
#
|
10
|
+
# Ruby script import procedure.
|
11
|
+
#
|
12
|
+
def import(source)
|
13
|
+
if File.file?(source)
|
14
|
+
case File.extname(source)
|
15
|
+
when '.rb' # TODO: Other ruby extensions ?
|
16
|
+
load_ruby(source)
|
17
|
+
true
|
18
|
+
else
|
19
|
+
text = read(source)
|
20
|
+
if text !=~ /\A---/ # not YAML
|
21
|
+
load_ruby(source)
|
22
|
+
true
|
23
|
+
else
|
24
|
+
super(source) if defined?(super)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
else
|
28
|
+
super(source) if defined?(super)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# Load ruby file by simply evaluating it in the context
|
34
|
+
# of the Builder instance.
|
35
|
+
#
|
36
|
+
def load_ruby(file)
|
37
|
+
instance_eval(File.read(file))
|
38
|
+
end
|
39
|
+
|
40
|
+
end #module RubyImportation
|
41
|
+
|
42
|
+
# Include RubyImportation mixin into Builder class.
|
43
|
+
include RubyImportation
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Indexer
|
2
|
+
|
3
|
+
class Importer
|
4
|
+
|
5
|
+
# Build mixin for Bundler's Gemfile.
|
6
|
+
#
|
7
|
+
module VersionImportation
|
8
|
+
#
|
9
|
+
# If the source file is a Gemfile, import it.
|
10
|
+
#
|
11
|
+
def import(source)
|
12
|
+
case source
|
13
|
+
when 'VERSION.txt', 'Version.txt'
|
14
|
+
vers = File.read(source).strip
|
15
|
+
metadata.version = vers
|
16
|
+
when 'VERSION', 'Version'
|
17
|
+
text = File.read(source).strip
|
18
|
+
if yaml?(text)
|
19
|
+
# don't really like this style b/c it's too subjective
|
20
|
+
hash = YAML.load(text)
|
21
|
+
hash = hash.inject{ |h,(k,v)| h[k.to_sym] = v; h }
|
22
|
+
vers = hash.values_at(:major,:minor,:patch,:build).compact
|
23
|
+
else
|
24
|
+
vers = File.read(source).strip
|
25
|
+
end
|
26
|
+
metadata.version = vers
|
27
|
+
else
|
28
|
+
super(source) if defined?(super)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Include VersionImportation mixin into Builder class.
|
34
|
+
include VersionImportation
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Indexer
|
2
|
+
|
3
|
+
class Importer
|
4
|
+
|
5
|
+
# Import metadata from a YAML source.
|
6
|
+
#
|
7
|
+
module YAMLImportation
|
8
|
+
|
9
|
+
#
|
10
|
+
# YAML import procedure.
|
11
|
+
#
|
12
|
+
def import(source)
|
13
|
+
if File.file?(source)
|
14
|
+
case File.extname(source)
|
15
|
+
when '.yaml', '.yml'
|
16
|
+
load_yaml(source)
|
17
|
+
true
|
18
|
+
else
|
19
|
+
text = read(source)
|
20
|
+
if text =~ /\A---/
|
21
|
+
load_yaml(source)
|
22
|
+
true
|
23
|
+
else
|
24
|
+
super(source) if defined?(super)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
else
|
28
|
+
super(source) if defined?(super)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# Import metadata from YAML file.
|
34
|
+
#
|
35
|
+
def load_yaml(file)
|
36
|
+
metadata.merge!(YAML.load_file(file))
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
# Include YAMLImportation mixin into Builder class.
|
42
|
+
include YAMLImportation
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
module Indexer
|
2
|
+
|
3
|
+
module Loadable
|
4
|
+
|
5
|
+
#
|
6
|
+
# Open metadata file and ensure strict validation to canonical format.
|
7
|
+
#
|
8
|
+
# @param [String] file or directory
|
9
|
+
# The file name from which to read the YAML metadata,
|
10
|
+
# or a directory from which to lookup the file.
|
11
|
+
#
|
12
|
+
def open(file=Dir.pwd)
|
13
|
+
file = find(file) if File.directory?(file)
|
14
|
+
valid(YAML.load_file(file))
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# Like #open, but do not ensure strict validation to canonical format.
|
19
|
+
#
|
20
|
+
# @param [String] file or directory
|
21
|
+
# The file name from which to read the YAML metadata,
|
22
|
+
# or a directory from which to lookup the file.
|
23
|
+
#
|
24
|
+
def read(file=Dir.pwd)
|
25
|
+
file = find(file) if File.directory?(file)
|
26
|
+
new(YAML.load_file(file))
|
27
|
+
end
|
28
|
+
|
29
|
+
#
|
30
|
+
# Create new Metadata instance from atypical sources.
|
31
|
+
#
|
32
|
+
# TODO: Use Importer to construct Metadata instance.
|
33
|
+
#
|
34
|
+
def import(*sources)
|
35
|
+
end
|
36
|
+
|
37
|
+
#
|
38
|
+
# Load from YAML string or IO.
|
39
|
+
#
|
40
|
+
# @param [String,#read] String or IO object
|
41
|
+
# The file name from which to read the YAML metadata.
|
42
|
+
#
|
43
|
+
def load(io)
|
44
|
+
new(YAML.load(io))
|
45
|
+
end
|
46
|
+
|
47
|
+
#
|
48
|
+
# Load from YAML file.
|
49
|
+
#
|
50
|
+
# @param [String] file
|
51
|
+
# The file name from which to read the YAML metadata.
|
52
|
+
#
|
53
|
+
def load_file(file)
|
54
|
+
new(YAML.load_file(file))
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
# Find project root and read the index file.
|
59
|
+
#
|
60
|
+
# @param [String] from
|
61
|
+
# The directory from which to start the upward search.
|
62
|
+
#
|
63
|
+
def find(from=Dir.pwd)
|
64
|
+
File.join(root(from), LOCK_FILE)
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Find project root by looking upward for a locked metadata file.
|
69
|
+
#
|
70
|
+
# @param [String] from
|
71
|
+
# The directory from which to start the upward search.
|
72
|
+
#
|
73
|
+
# @return [String]
|
74
|
+
# The path to the locked metadata file.
|
75
|
+
#
|
76
|
+
# @raise [Errno::ENOENT]
|
77
|
+
# The locked metadata file could not be located.
|
78
|
+
#
|
79
|
+
def root(from=Dir.pwd)
|
80
|
+
if not path = exists?(from)
|
81
|
+
lock_file_missing(from)
|
82
|
+
end
|
83
|
+
path
|
84
|
+
end
|
85
|
+
|
86
|
+
#
|
87
|
+
# Does a locked metadata file exist?
|
88
|
+
#
|
89
|
+
# @return [true,false] Whether locked metadata file exists.
|
90
|
+
#
|
91
|
+
def exists?(from=Dir.pwd)
|
92
|
+
home = File.expand_path('~')
|
93
|
+
path = File.expand_path(from)
|
94
|
+
while path != '/' and path != home
|
95
|
+
if File.file?(File.join(path,LOCK_FILE))
|
96
|
+
return path
|
97
|
+
else
|
98
|
+
path = File.dirname(path)
|
99
|
+
end
|
100
|
+
false #lock_file_missing(from)
|
101
|
+
end
|
102
|
+
false #lock_file_missing(from)
|
103
|
+
end
|
104
|
+
|
105
|
+
#
|
106
|
+
# Raise lock file missing error.
|
107
|
+
#
|
108
|
+
def lock_file_missing(from=nil)
|
109
|
+
raise Error.exception("could not locate .index file", Errno::ENOENT)
|
110
|
+
end
|
111
|
+
|
112
|
+
#
|
113
|
+
# Alias for exists?
|
114
|
+
#
|
115
|
+
def exist?(from=Dir.pwd)
|
116
|
+
exists?(from)
|
117
|
+
end
|
118
|
+
|
119
|
+
#
|
120
|
+
# Lockdown the metadata (via Importer) and return updated metadata.
|
121
|
+
#
|
122
|
+
# Options :force
|
123
|
+
#
|
124
|
+
def lock(*sources)
|
125
|
+
opts = (Hash === sources.last ? sources.pop : {})
|
126
|
+
|
127
|
+
file = nil
|
128
|
+
needed = true
|
129
|
+
sources = sources.flatten
|
130
|
+
|
131
|
+
if sources.empty?
|
132
|
+
if file = exists?
|
133
|
+
metadata = Metadata.open
|
134
|
+
sources = metadata.sources
|
135
|
+
else
|
136
|
+
sources = Dir.glob(USER_FILES, File::FNM_CASEFOLD)
|
137
|
+
raise Error.exception("could not find default sources") if sources.empty?
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
if file && !opts[:force]
|
142
|
+
date = sources.map{ |s| File.mtime(s) }.max
|
143
|
+
needed = false if File.mtime(file) > date
|
144
|
+
end
|
145
|
+
|
146
|
+
Importer.import(*sources) if needed
|
147
|
+
end
|
148
|
+
|
149
|
+
#
|
150
|
+
# Lockdown the metadata (via Importer) and save.
|
151
|
+
#
|
152
|
+
def lock!(*sources)
|
153
|
+
metadata = lock(*sources)
|
154
|
+
metadata.save! if metadata
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
@@ -0,0 +1,879 @@
|
|
1
|
+
require_relative 'loadable'
|
2
|
+
require_relative 'attributes'
|
3
|
+
require_relative 'conversion'
|
4
|
+
require_relative 'validator'
|
5
|
+
|
6
|
+
module Indexer
|
7
|
+
|
8
|
+
# Conventional interface for specification provides convenience
|
9
|
+
# for developers. It offers method aliases and models various parts
|
10
|
+
# of the specification with useful classes.
|
11
|
+
#
|
12
|
+
class Metadata < Model
|
13
|
+
extend Loadable
|
14
|
+
|
15
|
+
include Attributes
|
16
|
+
include Conversion
|
17
|
+
|
18
|
+
class << self
|
19
|
+
private
|
20
|
+
alias :create :new
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# Revision factory returns a versioned instance of the model class.
|
25
|
+
#
|
26
|
+
# @param [Hash] data
|
27
|
+
# The data to populate the instance.
|
28
|
+
#
|
29
|
+
def self.new(data={})
|
30
|
+
data = revise(data)
|
31
|
+
super(data)
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
# Load metadata, ensuring canoncial validity.
|
36
|
+
#
|
37
|
+
# @param [String] data
|
38
|
+
# The data to be validate and then to populate the instance.
|
39
|
+
#
|
40
|
+
def self.valid(data)
|
41
|
+
data = revise(data)
|
42
|
+
data = Validator.new(data).to_h
|
43
|
+
create(data)
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# Update metadata to current revision if using old revision.
|
48
|
+
#
|
49
|
+
def self.revise(data)
|
50
|
+
Revision.upconvert(data)
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# Create a new Meta::Spec given a Gem::Specification or .gemspec file.
|
55
|
+
#
|
56
|
+
# @param [Gem::Specification,String] gemspec
|
57
|
+
# RubyGems Gem::Specification object or path to .gemspec file.
|
58
|
+
#
|
59
|
+
def self.from_gemspec(gemspec)
|
60
|
+
new.import_gemspec(gemspec)
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
# -- Writers ------------------------------------------------------------
|
65
|
+
|
66
|
+
#
|
67
|
+
# Set the revision. This is in a sense a dummy setting, since the actual
|
68
|
+
# revision is alwasy the latest.
|
69
|
+
#
|
70
|
+
def revision=(value)
|
71
|
+
@data['revision'] = value.to_i
|
72
|
+
end
|
73
|
+
|
74
|
+
#
|
75
|
+
# Set the revision. This is in a sense a dummy setting, since the actual
|
76
|
+
# revision is alwasy the latest.
|
77
|
+
#
|
78
|
+
def type=(value)
|
79
|
+
Valid.type!(value, :type)
|
80
|
+
@data['type'] = type.to_str
|
81
|
+
end
|
82
|
+
|
83
|
+
#
|
84
|
+
# Sources for building index file.
|
85
|
+
#
|
86
|
+
# @param [String, Array] path(s)
|
87
|
+
# Paths from which metadata can be extracted.
|
88
|
+
#
|
89
|
+
def sources=(list)
|
90
|
+
@data[:sources] = [list].flatten
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
alias :source :sources
|
95
|
+
alias :source= :sources=
|
96
|
+
|
97
|
+
#
|
98
|
+
# Sets the name of the project.
|
99
|
+
#
|
100
|
+
# @param [String] name
|
101
|
+
# The new name of the project.
|
102
|
+
#
|
103
|
+
def name=(name)
|
104
|
+
name = name.to_s if Symbol === name
|
105
|
+
Valid.name!(name, :name)
|
106
|
+
@data[:name] = name.to_str.downcase
|
107
|
+
@data[:title] = @data[:name].capitalize unless @data[:title] # TODO: use #titlecase
|
108
|
+
@data[:name]
|
109
|
+
end
|
110
|
+
|
111
|
+
#
|
112
|
+
# Title is sanitized so that all white space is reduced to a
|
113
|
+
# single space character.
|
114
|
+
#
|
115
|
+
def title=(title)
|
116
|
+
Valid.oneline!(title, :title)
|
117
|
+
@data[:title] = title.to_str.gsub(/\s+/, ' ')
|
118
|
+
end
|
119
|
+
|
120
|
+
#
|
121
|
+
# The toplevel namespace of API, e.g. `module Foo` or `class Bar`,
|
122
|
+
# would be `"Foo"` or `"Bar"`, repectively.
|
123
|
+
#
|
124
|
+
# @param [String] namespace
|
125
|
+
# The new toplevel namespace of the project's API.
|
126
|
+
#
|
127
|
+
def namespace=(namespace)
|
128
|
+
Valid.constant!(namespace)
|
129
|
+
@data[:namespace] = namespace
|
130
|
+
end
|
131
|
+
|
132
|
+
#
|
133
|
+
# Summary is sanitized to only have one line of text.
|
134
|
+
#
|
135
|
+
def summary=(summary)
|
136
|
+
Valid.string!(summary, :summary)
|
137
|
+
@data[:summary] = summary.to_str.gsub(/\s+/, ' ')
|
138
|
+
end
|
139
|
+
|
140
|
+
#
|
141
|
+
# Sets the version of the project.
|
142
|
+
#
|
143
|
+
# @param [Hash, String, Array, Version::Number] version
|
144
|
+
# The version from the metadata file.
|
145
|
+
#
|
146
|
+
# @raise [ValidationError]
|
147
|
+
# The version must either be a `String`, `Hash` or `Array`.
|
148
|
+
#
|
149
|
+
def version=(version)
|
150
|
+
case version
|
151
|
+
when Version::Number
|
152
|
+
@data[:version] = version
|
153
|
+
when Hash
|
154
|
+
major = version['major'] || version[:major]
|
155
|
+
minor = version['minor'] || version[:minor]
|
156
|
+
patch = version['patch'] || version[:patch]
|
157
|
+
build = version['build'] || version[:build]
|
158
|
+
@data[:version] = Version::Number.new(major,minor,patch,build)
|
159
|
+
when String
|
160
|
+
@data[:version] = Version::Number.parse(version.to_s)
|
161
|
+
when Array
|
162
|
+
@data[:version] = Version::Number.new(*version)
|
163
|
+
else
|
164
|
+
raise(ValidationError,"version must be a Hash or a String")
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
#
|
169
|
+
# Codename is the name of the particular version.
|
170
|
+
#
|
171
|
+
def codename=(codename)
|
172
|
+
codename = codename.to_s if Symbol === codename
|
173
|
+
Valid.oneline!(codename, :codename)
|
174
|
+
@data[:codename] = codename.to_str
|
175
|
+
end
|
176
|
+
|
177
|
+
#
|
178
|
+
# Sets the production date of the project.
|
179
|
+
#
|
180
|
+
# @param [String,Date,Time,DateTime] date
|
181
|
+
# The production date for this version.
|
182
|
+
#
|
183
|
+
def date=(date)
|
184
|
+
@data[:date] = \
|
185
|
+
case date
|
186
|
+
when String
|
187
|
+
begin
|
188
|
+
Date.parse(date)
|
189
|
+
rescue ArgumentError
|
190
|
+
raise ValidationError, "invalid date for `date' - #{date.inspect}"
|
191
|
+
end
|
192
|
+
when Date, Time, DateTime
|
193
|
+
date
|
194
|
+
else
|
195
|
+
raise ValidationError, "invalid date for `date' - #{date.inspect}"
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
#
|
200
|
+
# Sets the creation date of the project.
|
201
|
+
#
|
202
|
+
# @param [String,Date,Time,DateTime] date
|
203
|
+
# The creation date of this project.
|
204
|
+
#
|
205
|
+
def created=(date)
|
206
|
+
@data[:created] = \
|
207
|
+
case date
|
208
|
+
when String
|
209
|
+
Valid.utc_date!(date)
|
210
|
+
Date.parse(date)
|
211
|
+
when Date, Time, DateTime
|
212
|
+
date
|
213
|
+
else
|
214
|
+
raise ValidationError, "invalid date for `created' - #{date.inspect}"
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
# Set the copyrights and licenses for the project.
|
219
|
+
#
|
220
|
+
# Copyrights SHOULD be in order of significance. The first license
|
221
|
+
# given is taken to be the project's primary license.
|
222
|
+
#
|
223
|
+
# License fields SHOULD be offically recognized identifiers such
|
224
|
+
# as "GPL-3.0" as defined by SPDX (http://).
|
225
|
+
#
|
226
|
+
# @example
|
227
|
+
# spec.copyrights = [
|
228
|
+
# {
|
229
|
+
# 'year' => '2010',
|
230
|
+
# 'holder' => 'Thomas T. Thomas',
|
231
|
+
# 'license' => "MIT"
|
232
|
+
# }
|
233
|
+
# ]
|
234
|
+
#
|
235
|
+
# @param [Array<Hash,String,Array>]
|
236
|
+
# The copyrights and licenses of the project.
|
237
|
+
#
|
238
|
+
def copyrights=(copyrights)
|
239
|
+
@data[:copyrights] = \
|
240
|
+
case copyrights
|
241
|
+
when String
|
242
|
+
[Copyright.parse(copyrights, @_license)]
|
243
|
+
when Hash
|
244
|
+
[Copyright.parse(copyrights, @_license)]
|
245
|
+
when Array
|
246
|
+
copyrights.map do |copyright|
|
247
|
+
Copyright.parse(copyright, @_license)
|
248
|
+
end
|
249
|
+
else
|
250
|
+
raise(ValidationError, "copyright must be a String, Hash or Array")
|
251
|
+
end
|
252
|
+
@data[:copyrights]
|
253
|
+
end
|
254
|
+
|
255
|
+
#
|
256
|
+
# Singular form of `#copyrights=`. This is similar to `#copyrights=`
|
257
|
+
# but expects the parameter to represent only one copyright.
|
258
|
+
#
|
259
|
+
def copyright=(copyright)
|
260
|
+
@data[:copyrights] = [Copyright.parse(copyright)]
|
261
|
+
end
|
262
|
+
|
263
|
+
# TODO: Should their be a "primary" license field ?
|
264
|
+
|
265
|
+
#
|
266
|
+
# Set copyright license for all copyright holders.
|
267
|
+
#
|
268
|
+
def license=(license)
|
269
|
+
if copyrights = @data[:copyrights]
|
270
|
+
copyrights.each do |c|
|
271
|
+
c.license = license # TODO: unless c.license ?
|
272
|
+
end
|
273
|
+
end
|
274
|
+
@_license = license
|
275
|
+
end
|
276
|
+
|
277
|
+
# Set the authors of the project.
|
278
|
+
#
|
279
|
+
# @param [Array<String>, String] authors
|
280
|
+
# The originating authors of the project.
|
281
|
+
#
|
282
|
+
def authors=(authors)
|
283
|
+
@data[:authors] = (
|
284
|
+
list = Array(authors).map do |a|
|
285
|
+
Author.parse(a)
|
286
|
+
end
|
287
|
+
warn "Duplicate authors listed" if list != list.uniq
|
288
|
+
list
|
289
|
+
)
|
290
|
+
end
|
291
|
+
|
292
|
+
#
|
293
|
+
alias author= authors=
|
294
|
+
|
295
|
+
#
|
296
|
+
# Set the orgnaization to which the project belongs.
|
297
|
+
#
|
298
|
+
# @param [String] organization
|
299
|
+
# The name of the organization.
|
300
|
+
#
|
301
|
+
def organizations=(organizations)
|
302
|
+
@data[:organizations] = (
|
303
|
+
list = Array(organizations).map do |org|
|
304
|
+
Organization.parse(org)
|
305
|
+
end
|
306
|
+
warn "Duplicate organizations listed" if list != list.uniq
|
307
|
+
list
|
308
|
+
)
|
309
|
+
end
|
310
|
+
|
311
|
+
#
|
312
|
+
alias :organization= :organizations=
|
313
|
+
|
314
|
+
# Company is a typical synonym for organization.
|
315
|
+
alias :company :organizations
|
316
|
+
alias :company= :organizations=
|
317
|
+
alias :companies :organizations=
|
318
|
+
alias :companies= :organizations=
|
319
|
+
|
320
|
+
# TODO: should we warn if directory does not exist?
|
321
|
+
|
322
|
+
# Sets the require paths of the project.
|
323
|
+
#
|
324
|
+
# @param [Array<String>, String] paths
|
325
|
+
# The require-paths or a glob-pattern.
|
326
|
+
#
|
327
|
+
def load_path=(paths)
|
328
|
+
@data[:load_path] = \
|
329
|
+
Array(paths).map do |path|
|
330
|
+
Valid.path!(path)
|
331
|
+
path
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
# List of language engine/version family supported.
|
336
|
+
def engines=(value)
|
337
|
+
@data[:engines] = (
|
338
|
+
a = [value].flatten
|
339
|
+
a.each{ |x| Valid.oneline!(x) }
|
340
|
+
a
|
341
|
+
)
|
342
|
+
end
|
343
|
+
|
344
|
+
#
|
345
|
+
# List of platforms supported.
|
346
|
+
#
|
347
|
+
# @deprecated
|
348
|
+
#
|
349
|
+
def platforms=(value)
|
350
|
+
@data[:platforms] = (
|
351
|
+
a = [value].flatten
|
352
|
+
a.each{ |x| Valid.oneline!(x) }
|
353
|
+
a
|
354
|
+
)
|
355
|
+
end
|
356
|
+
|
357
|
+
#
|
358
|
+
# Sets the requirements of the project. Also commonly called dependencies.
|
359
|
+
#
|
360
|
+
# @param [Array<Hash>, Hash{String=>Hash}, Hash{String=>String}] requirements
|
361
|
+
# The requirement details.
|
362
|
+
#
|
363
|
+
# @raise [ValidationError]
|
364
|
+
# The requirements must be an `Array` or `Hash`.
|
365
|
+
#
|
366
|
+
def requirements=(requirements)
|
367
|
+
requirements = [requirements] if String === requirements
|
368
|
+
case requirements
|
369
|
+
when Array, Hash
|
370
|
+
@data[:requirements].clear
|
371
|
+
requirements.each do |specifics|
|
372
|
+
@data[:requirements] << Requirement.parse(specifics)
|
373
|
+
end
|
374
|
+
else
|
375
|
+
raise(ValidationError,"requirements must be an Array or Hash")
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
#
|
380
|
+
# Dependencies is an alias for requirements.
|
381
|
+
#
|
382
|
+
alias :dependencies :requirements
|
383
|
+
alias :dependencies= :requirements=
|
384
|
+
|
385
|
+
#
|
386
|
+
# Sets the packages with which this package is known to have
|
387
|
+
# incompatibilites.
|
388
|
+
#
|
389
|
+
# @param [Array<Hash>, Hash{String=>String}] conflicts
|
390
|
+
# The conflicts for the project.
|
391
|
+
#
|
392
|
+
# @raise [ValidationError]
|
393
|
+
# The conflicts list must be an `Array` or `Hash`.
|
394
|
+
#
|
395
|
+
# @todo lets get rid of the type check here and let the #parse method do it.
|
396
|
+
#
|
397
|
+
def conflicts=(conflicts)
|
398
|
+
case conflicts
|
399
|
+
when Array, Hash
|
400
|
+
@data[:conflicts].clear
|
401
|
+
conflicts.each do |specifics|
|
402
|
+
@data[:conflicts] << Conflict.parse(specifics)
|
403
|
+
end
|
404
|
+
else
|
405
|
+
raise(ValidationError, "conflicts must be an Array or Hash")
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
#
|
410
|
+
# Sets the packages this package could (more or less) replace.
|
411
|
+
#
|
412
|
+
# @param [Array<String>] alternatives
|
413
|
+
# The alternatives for the project.
|
414
|
+
#
|
415
|
+
def alternatives=(alternatives)
|
416
|
+
Valid.array!(alternatives, :alternatives)
|
417
|
+
|
418
|
+
@data[:alternatives].clear
|
419
|
+
|
420
|
+
alternatives.to_ary.each do |name|
|
421
|
+
@data[:alternatives] << name.to_s
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
#
|
426
|
+
# Sets the categories for this project.
|
427
|
+
#
|
428
|
+
# @param [Array<String>] categories
|
429
|
+
# List of purpose categories for the project.
|
430
|
+
#
|
431
|
+
def categories=(categories)
|
432
|
+
categories = Array(categories)
|
433
|
+
|
434
|
+
@data[:categories].clear
|
435
|
+
|
436
|
+
categories.to_ary.each do |name|
|
437
|
+
Valid.oneline!(name.to_s)
|
438
|
+
@data[:categories] << name.to_s
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
#
|
443
|
+
# Suite must be a single line string.
|
444
|
+
#
|
445
|
+
# @param [String] suite
|
446
|
+
# The suite to which the project belongs.
|
447
|
+
#
|
448
|
+
def suite=(value)
|
449
|
+
Valid.oneline!(value, :suite)
|
450
|
+
@data[:suite] = value
|
451
|
+
end
|
452
|
+
|
453
|
+
#
|
454
|
+
# Sets the repostiories this project has.
|
455
|
+
#
|
456
|
+
# @param [Array<String>, Hash] repositories
|
457
|
+
# The repositories for the project.
|
458
|
+
#
|
459
|
+
def repositories=(repositories)
|
460
|
+
case repositories
|
461
|
+
when Hash, Array
|
462
|
+
@data[:repositories].clear
|
463
|
+
repositories.each do |specifics|
|
464
|
+
@data[:repositories] << Repository.parse(specifics)
|
465
|
+
end
|
466
|
+
else
|
467
|
+
raise(ValidationError, "repositories must be an Array or Hash")
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
#
|
472
|
+
# Set the resources for the project.
|
473
|
+
#
|
474
|
+
# @param [Array,Hash] resources
|
475
|
+
# A list or map of resources.
|
476
|
+
#
|
477
|
+
def resources=(resources)
|
478
|
+
case resources
|
479
|
+
when Array
|
480
|
+
@data[:resources].clear
|
481
|
+
resources.each do |data|
|
482
|
+
@data[:resources] << Resource.parse(data)
|
483
|
+
end
|
484
|
+
when Hash
|
485
|
+
@data[:resources].clear
|
486
|
+
resources.each do |type, uri|
|
487
|
+
@data[:resources] << Resource.new(:uri=>uri, :type=>type.to_s)
|
488
|
+
end
|
489
|
+
else
|
490
|
+
raise(ValidationError, "repositories must be an Array or Hash")
|
491
|
+
end
|
492
|
+
end
|
493
|
+
|
494
|
+
#
|
495
|
+
# The webcvs prefix must be a valid URI.
|
496
|
+
#
|
497
|
+
# @param [String] uri
|
498
|
+
# The webcvs prefix, which must be a valid URI.
|
499
|
+
#
|
500
|
+
def webcvs=(uri)
|
501
|
+
Valid.uri!(uri, :webcvs)
|
502
|
+
@data[:webcvs] = uri
|
503
|
+
end
|
504
|
+
|
505
|
+
#
|
506
|
+
# Sets the post-install message of the project.
|
507
|
+
#
|
508
|
+
# @param [Array, String] message
|
509
|
+
# The post-installation message.
|
510
|
+
#
|
511
|
+
# @return [String]
|
512
|
+
# The new post-installation message.
|
513
|
+
#
|
514
|
+
def install_message=(message)
|
515
|
+
@data[:install_message] = \
|
516
|
+
case message
|
517
|
+
when Array
|
518
|
+
message.join($/)
|
519
|
+
else
|
520
|
+
Valid.string!(message)
|
521
|
+
message.to_str
|
522
|
+
end
|
523
|
+
end
|
524
|
+
|
525
|
+
##
|
526
|
+
## Set extraneous developer-defined metdata.
|
527
|
+
##
|
528
|
+
#def extra=(extra)
|
529
|
+
# unless extra.kind_of?(Hash)
|
530
|
+
# raise(ValidationError, "extra must be a Hash")
|
531
|
+
# end
|
532
|
+
# @data[:extra] = extra
|
533
|
+
#end
|
534
|
+
|
535
|
+
# -- Utility Methods ----------------------------------------------------
|
536
|
+
|
537
|
+
#
|
538
|
+
# Adds a new requirement.
|
539
|
+
#
|
540
|
+
# @param [String] name
|
541
|
+
# The name of the requirement.
|
542
|
+
#
|
543
|
+
# @param [Hash] specifics
|
544
|
+
# The specifics of the requirement.
|
545
|
+
#
|
546
|
+
def add_requirement(name, specifics)
|
547
|
+
requirements << Requirement.parse([name, specifics])
|
548
|
+
end
|
549
|
+
|
550
|
+
#
|
551
|
+
# Same as #add_requirement.
|
552
|
+
#
|
553
|
+
# @param [String] name
|
554
|
+
# The name of the dependency.
|
555
|
+
#
|
556
|
+
# @param [Hash] specifics
|
557
|
+
# The specifics of the dependency.
|
558
|
+
#
|
559
|
+
alias add_dependency add_requirement
|
560
|
+
|
561
|
+
#
|
562
|
+
# Adds a new conflict.
|
563
|
+
#
|
564
|
+
# @param [String] name
|
565
|
+
# The name of the conflict package.
|
566
|
+
#
|
567
|
+
# @param [Hash] specifics
|
568
|
+
# The specifics of the conflict package.
|
569
|
+
#
|
570
|
+
def add_conflict(name, specifics)
|
571
|
+
conflicts << Requirement.parse([name, specifics])
|
572
|
+
end
|
573
|
+
|
574
|
+
#
|
575
|
+
# Adds a new alternative.
|
576
|
+
#
|
577
|
+
# @param [String] name
|
578
|
+
# The name of the alternative.
|
579
|
+
#
|
580
|
+
def add_alternative(name)
|
581
|
+
alternatives << name.to_s
|
582
|
+
end
|
583
|
+
|
584
|
+
#
|
585
|
+
#
|
586
|
+
#
|
587
|
+
def add_repository(id, url, scm=nil)
|
588
|
+
repositories << Repository.parse(:id=>id, :url=>url, :scm=>scm)
|
589
|
+
end
|
590
|
+
|
591
|
+
#
|
592
|
+
# A specification is not valid without a name and version.
|
593
|
+
#
|
594
|
+
# @return [Boolean] valid specification?
|
595
|
+
#
|
596
|
+
def valid?
|
597
|
+
return false unless name
|
598
|
+
return false unless version
|
599
|
+
true
|
600
|
+
end
|
601
|
+
|
602
|
+
# TODO: What was used for again, load_path ?
|
603
|
+
=begin
|
604
|
+
#
|
605
|
+
# Iterates over the paths.
|
606
|
+
#
|
607
|
+
# @param [Array<String>, String] paths
|
608
|
+
# The paths or path glob pattern to iterate over.
|
609
|
+
#
|
610
|
+
# @yield [path]
|
611
|
+
# The given block will be passed each individual path.
|
612
|
+
#
|
613
|
+
# @yieldparam [String] path
|
614
|
+
# An individual path.
|
615
|
+
#
|
616
|
+
def each_path(paths,&block)
|
617
|
+
case paths
|
618
|
+
when Array
|
619
|
+
paths.each(&block)
|
620
|
+
when String
|
621
|
+
Dir.glob(paths,&block) # TODO: should we be going this?
|
622
|
+
else
|
623
|
+
raise(ValidationError, "invalid path")
|
624
|
+
end
|
625
|
+
end
|
626
|
+
|
627
|
+
private :each_path
|
628
|
+
=end
|
629
|
+
|
630
|
+
# -- Calculations -------------------------------------------------------
|
631
|
+
|
632
|
+
#
|
633
|
+
# The primary copyright of the project.
|
634
|
+
#
|
635
|
+
# @return [String]
|
636
|
+
# The primary copyright for the project.
|
637
|
+
#
|
638
|
+
def copyright
|
639
|
+
copyrights.join("\n")
|
640
|
+
end
|
641
|
+
|
642
|
+
#
|
643
|
+
# The primary email address of the project. The email address
|
644
|
+
# is taken from the first listed author.
|
645
|
+
#
|
646
|
+
# @return [String, nil]
|
647
|
+
# The primary email address for the project.
|
648
|
+
#
|
649
|
+
def email
|
650
|
+
authors.find{ |a| a.email }
|
651
|
+
end
|
652
|
+
|
653
|
+
#
|
654
|
+
# Get homepage URI.
|
655
|
+
#
|
656
|
+
def homepage #(uri=nil)
|
657
|
+
#uri ? self.homepage = url
|
658
|
+
resources.each do |r|
|
659
|
+
if r.type == 'home'
|
660
|
+
return r.uri
|
661
|
+
end
|
662
|
+
end
|
663
|
+
end
|
664
|
+
alias_method :website, :homepage
|
665
|
+
|
666
|
+
#
|
667
|
+
# Convenience method for setting a `hompage` resource.
|
668
|
+
#
|
669
|
+
# @todo Rename to website?
|
670
|
+
#
|
671
|
+
def homepage=(uri)
|
672
|
+
resource = Resource.new(:name=>'homepage', :type=>'home', :uri=>uri)
|
673
|
+
resources.unshift(resource)
|
674
|
+
uri
|
675
|
+
end
|
676
|
+
alias_method :website=, :homepage=
|
677
|
+
|
678
|
+
#
|
679
|
+
# Returns the runtime requirements of the project.
|
680
|
+
#
|
681
|
+
# @return [Array<Requirement>] runtime requirements.
|
682
|
+
def runtime_requirements
|
683
|
+
requirements.select do |requirement|
|
684
|
+
requirement.runtime?
|
685
|
+
end
|
686
|
+
end
|
687
|
+
|
688
|
+
#
|
689
|
+
# Returns the development requirements of the project.
|
690
|
+
#
|
691
|
+
# @return [Array<Requirement>] development requirements.
|
692
|
+
#
|
693
|
+
def development_requirements
|
694
|
+
requirements.select do |requirement|
|
695
|
+
requirement.development?
|
696
|
+
end
|
697
|
+
end
|
698
|
+
|
699
|
+
#
|
700
|
+
# Returns the runtime requirements of the project.
|
701
|
+
#
|
702
|
+
# @return [Array<Requirement>] runtime requirements.
|
703
|
+
def external_requirements
|
704
|
+
requirements.select do |requirement|
|
705
|
+
requirement.external?
|
706
|
+
end
|
707
|
+
end
|
708
|
+
|
709
|
+
#
|
710
|
+
# Returns the external runtime requirements of the project.
|
711
|
+
#
|
712
|
+
# @return [Array<Requirement>] external runtime requirements.
|
713
|
+
def external_runtime_requirements
|
714
|
+
requirements.select do |requirement|
|
715
|
+
requirement.external? && requirement.runtime?
|
716
|
+
end
|
717
|
+
end
|
718
|
+
|
719
|
+
#
|
720
|
+
# Returns the external development requirements of the project.
|
721
|
+
#
|
722
|
+
# @return [Array<Requirement>] external development requirements.
|
723
|
+
def external_development_requirements
|
724
|
+
requirements.select do |requirement|
|
725
|
+
requirement.external? && requirement.development?
|
726
|
+
end
|
727
|
+
end
|
728
|
+
|
729
|
+
# -- Aliases ------------------------------------------------------------
|
730
|
+
|
731
|
+
#
|
732
|
+
#
|
733
|
+
# @return [Array] load paths
|
734
|
+
def loadpath
|
735
|
+
load_path
|
736
|
+
end
|
737
|
+
|
738
|
+
#
|
739
|
+
# RubyGems term for #load_path.
|
740
|
+
#
|
741
|
+
def loadpath=(value)
|
742
|
+
self.load_path = value
|
743
|
+
end
|
744
|
+
|
745
|
+
#
|
746
|
+
#
|
747
|
+
# @return [Array] load paths
|
748
|
+
def require_paths
|
749
|
+
load_path
|
750
|
+
end
|
751
|
+
|
752
|
+
#
|
753
|
+
# RubyGems term for #load_path.
|
754
|
+
#
|
755
|
+
def require_paths=(value)
|
756
|
+
self.load_path = value
|
757
|
+
end
|
758
|
+
|
759
|
+
#
|
760
|
+
# Alternate short name for #requirements.
|
761
|
+
#
|
762
|
+
def requires
|
763
|
+
requirements
|
764
|
+
end
|
765
|
+
|
766
|
+
#
|
767
|
+
# Alternate short name for #requirements.
|
768
|
+
#
|
769
|
+
def requires=(value)
|
770
|
+
self.requirements = value
|
771
|
+
end
|
772
|
+
|
773
|
+
#
|
774
|
+
# Alternate singular form of #engines.
|
775
|
+
#
|
776
|
+
def engine=(value)
|
777
|
+
self.engines = value
|
778
|
+
end
|
779
|
+
|
780
|
+
#
|
781
|
+
# Alternate singular form of #platforms.
|
782
|
+
#
|
783
|
+
def platform=(value)
|
784
|
+
self.platforms = value
|
785
|
+
end
|
786
|
+
|
787
|
+
# -- Conversion ---------------------------------------------------------
|
788
|
+
|
789
|
+
#
|
790
|
+
# Convert convenience form of metadata to canonical form.
|
791
|
+
#
|
792
|
+
# @todo: A more robust means of insuring sources never self references.
|
793
|
+
#
|
794
|
+
# @todo: Make sure this *always* generates the canonical form.
|
795
|
+
#
|
796
|
+
def to_h
|
797
|
+
date = self.date || Time.now
|
798
|
+
|
799
|
+
h = super
|
800
|
+
|
801
|
+
h['revision'] = REVISION
|
802
|
+
h['sources' ] = sources - ['.index'] # avoid self reference
|
803
|
+
|
804
|
+
h['version'] = version.to_s
|
805
|
+
|
806
|
+
h['date'] = date.strftime('%Y-%m-%d')
|
807
|
+
h['created'] = created.strftime('%Y-%m-%d') if created
|
808
|
+
|
809
|
+
h['authors'] = authors.map { |x| x.to_h }
|
810
|
+
h['organizations'] = organizations.map { |x| x.to_h }
|
811
|
+
h['copyrights'] = copyrights.map { |x| x.to_h }
|
812
|
+
h['requirements'] = requirements.map { |x| x.to_h }
|
813
|
+
h['conflicts'] = conflicts.map { |x| x.to_h }
|
814
|
+
h['repositories'] = repositories.map { |x| x.to_h }
|
815
|
+
h['resources'] = resources.map { |x| x.to_h }
|
816
|
+
|
817
|
+
h
|
818
|
+
end
|
819
|
+
|
820
|
+
# Create nicely formated project "about" text.
|
821
|
+
#
|
822
|
+
# @return [String] Formatted about text.
|
823
|
+
#
|
824
|
+
def about(*parts)
|
825
|
+
s = []
|
826
|
+
parts = [:header, :description, :resources, :copyright] if parts.empty?
|
827
|
+
parts.each do |part|
|
828
|
+
case part.to_sym
|
829
|
+
when :header
|
830
|
+
s << "%s %s (%s-%s)" % [title, version, name, version]
|
831
|
+
when :title
|
832
|
+
s << title
|
833
|
+
when :package
|
834
|
+
s << "%s-%s" % [name, version]
|
835
|
+
when :description
|
836
|
+
s << description || summary
|
837
|
+
when :summary
|
838
|
+
s << summary
|
839
|
+
when :resources
|
840
|
+
s << resources.map{ |resource|
|
841
|
+
"%s: %s" % [resource.label || resource.type, resource.uri]
|
842
|
+
}.join("\n")
|
843
|
+
when :repositories
|
844
|
+
s << repositories.map{ |repo|
|
845
|
+
"%s" % [repo.uri]
|
846
|
+
}.join("\n")
|
847
|
+
when :copyright, :copyrights
|
848
|
+
s << copyrights.map{ |c|
|
849
|
+
"Copyright (c) %s %s (%s)" % [c.year, c.holder, c.license]
|
850
|
+
}.join("\n")
|
851
|
+
else
|
852
|
+
s << __send__(part)
|
853
|
+
end
|
854
|
+
end
|
855
|
+
s.join("\n\n")
|
856
|
+
end
|
857
|
+
|
858
|
+
# Save metadata lock file.
|
859
|
+
#
|
860
|
+
# @param [String] file
|
861
|
+
# The file name in which to save the metadata as YAML.
|
862
|
+
#
|
863
|
+
def save!(file=LOCK_FILE)
|
864
|
+
v = Validator.new(to_h)
|
865
|
+
v.save!(file)
|
866
|
+
end
|
867
|
+
|
868
|
+
protected
|
869
|
+
|
870
|
+
#
|
871
|
+
# Initializes the {Metadata} attributes.
|
872
|
+
#
|
873
|
+
def initialize_attributes
|
874
|
+
super
|
875
|
+
end
|
876
|
+
|
877
|
+
end
|
878
|
+
|
879
|
+
end
|