indexer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/.index +54 -0
  2. data/HISTORY.md +9 -0
  3. data/README.md +145 -0
  4. data/bin/index +7 -0
  5. data/data/indexer/r2013/index.kwalify +175 -0
  6. data/data/indexer/r2013/index.yes +172 -0
  7. data/data/indexer/r2013/index.yesi +67 -0
  8. data/data/indexer/r2013/ruby.txt +35 -0
  9. data/data/indexer/r2013/yaml.txt +30 -0
  10. data/lib/indexer.rb +65 -0
  11. data/lib/indexer/attributes.rb +171 -0
  12. data/lib/indexer/command.rb +260 -0
  13. data/lib/indexer/components.rb +8 -0
  14. data/lib/indexer/components/author.rb +140 -0
  15. data/lib/indexer/components/conflict.rb +78 -0
  16. data/lib/indexer/components/copyright.rb +95 -0
  17. data/lib/indexer/components/dependency.rb +18 -0
  18. data/lib/indexer/components/organization.rb +133 -0
  19. data/lib/indexer/components/repository.rb +140 -0
  20. data/lib/indexer/components/requirement.rb +360 -0
  21. data/lib/indexer/components/resource.rb +209 -0
  22. data/lib/indexer/conversion.rb +14 -0
  23. data/lib/indexer/conversion/gemfile.rb +44 -0
  24. data/lib/indexer/conversion/gemspec.rb +114 -0
  25. data/lib/indexer/conversion/gemspec_exporter.rb +304 -0
  26. data/lib/indexer/core_ext.rb +4 -0
  27. data/lib/indexer/error.rb +23 -0
  28. data/lib/indexer/gemfile.rb +75 -0
  29. data/lib/indexer/importer.rb +144 -0
  30. data/lib/indexer/importer/file.rb +94 -0
  31. data/lib/indexer/importer/gemfile.rb +27 -0
  32. data/lib/indexer/importer/gemspec.rb +43 -0
  33. data/lib/indexer/importer/html.rb +289 -0
  34. data/lib/indexer/importer/markdown.rb +45 -0
  35. data/lib/indexer/importer/ruby.rb +47 -0
  36. data/lib/indexer/importer/version.rb +38 -0
  37. data/lib/indexer/importer/yaml.rb +46 -0
  38. data/lib/indexer/loadable.rb +159 -0
  39. data/lib/indexer/metadata.rb +879 -0
  40. data/lib/indexer/model.rb +237 -0
  41. data/lib/indexer/revision.rb +43 -0
  42. data/lib/indexer/valid.rb +287 -0
  43. data/lib/indexer/validator.rb +313 -0
  44. data/lib/indexer/version/constraint.rb +124 -0
  45. data/lib/indexer/version/exceptions.rb +11 -0
  46. data/lib/indexer/version/number.rb +497 -0
  47. metadata +141 -0
@@ -0,0 +1,209 @@
1
+ module Indexer
2
+
3
+ # A resource encapsulates information about a URI related to a project.
4
+ #
5
+ # TODO: This model is a little tricky b/c it's difficult to settle on a standard
6
+ # set of types or labels. It is also difficult to determine if label's and types
7
+ # should in fact be infered or just left empty if not provided. Any insight welcome!
8
+ #
9
+ class Resource < Model
10
+
11
+ # Parse `data` returning a new Resource instance.
12
+ #
13
+ # @param data [String,Array<String>,Array<Hash>,Hash]
14
+ # Resource information.
15
+ #
16
+ # @return [Resource] resource instance
17
+ def self.parse(data)
18
+ case data
19
+ when String
20
+ new(:uri=>data)
21
+ when Array
22
+ h, d = {}, data.dup # TODO: data.rekey(&:to_s)
23
+ h.update(d.pop) while Hash === d.last
24
+ if x = d.find{ |e| Valid.uri?(e) }
25
+ h[:uri] = d.delete(x)
26
+ end
27
+ if x = d.find{ |e| Valid.word?(e) && e.downcase == e }
28
+ h[:type] = d.delete(x)
29
+ end
30
+ h[:label] = d.shift unless d.empty?
31
+ raise ValidationError, "malformed resource -- #{data.inspect}" unless d.empty?
32
+ new(h)
33
+ when Hash
34
+ new(data)
35
+ else
36
+ raise(ValidationError, "not a valid resource")
37
+ end
38
+ end
39
+
40
+ #
41
+ # Initialize new Resource instance.
42
+ #
43
+ def initialize(data={})
44
+ @data = {}
45
+
46
+ data = data.rekey(&:to_sym)
47
+
48
+ ## best to assign in this order
49
+ self.uri = data.delete(:uri)
50
+ self.type = data.delete(:type) if data.key?(:type)
51
+
52
+ merge!(data)
53
+ end
54
+
55
+ #
56
+ # A label that can be used to identify the purpose of a
57
+ # particular repository.
58
+ #
59
+ attr :label
60
+
61
+ #
62
+ # Set the resource name. This can be any brief one line description.
63
+ #
64
+ # Examples
65
+ #
66
+ # resource.label = "Homepage"
67
+ #
68
+ def label=(label)
69
+ Valid.oneline!(label) # should be word!
70
+ @data[:type] = infer_type(label) unless @type
71
+ @data[:label] = label.to_str
72
+ end
73
+
74
+ #
75
+ # Alias for #label.
76
+ #
77
+ alias :name :label
78
+ alias :name= :label=
79
+
80
+ #
81
+ # The repository's URI.
82
+ #
83
+ attr :uri
84
+
85
+ #
86
+ # Set repository URI
87
+ #
88
+ def uri=(uri)
89
+ Valid.uri!(uri) # TODO: any other limitations?
90
+ @data[:type] = infer_type(uri) unless @type
91
+ @data[:uri] = uri
92
+ end
93
+
94
+ #
95
+ #
96
+ #
97
+ attr :type
98
+
99
+ #
100
+ # Set the type of the resource. The type is a single downcased word.
101
+ # It can be anything but generally recognized types are:
102
+ #
103
+ # * bugs, issues
104
+ # * mail, email
105
+ # * chat, irc
106
+ # * home, homepage, website
107
+ # * work, code
108
+ #
109
+ def type=(type)
110
+ Valid.word!(type)
111
+ @data[:label] = infer_label(type) unless @label
112
+ @data[:type] = type.to_str.downcase
113
+ end
114
+
115
+ ##
116
+ ## Convert resource to Hash.
117
+ ##
118
+ ## @return [Hash]
119
+ ##
120
+ #def to_h
121
+ # h = {}
122
+ # h['uri'] = uri
123
+ # h['label'] = label if label
124
+ # h['type'] = type if type
125
+ # h
126
+ #end
127
+
128
+ #
129
+ # Recognized types and the default labels that do with them.
130
+ #
131
+ TYPE_LABELS = {
132
+ 'work' => "Development",
133
+ 'dev' => "Development",
134
+ 'code' => "Source Code",
135
+ 'source' => "Source Code",
136
+ 'bugs' => "Issue Tracker",
137
+ 'issues' => "Issue Tracker",
138
+ 'mail' => "Mailing List",
139
+ 'list' => "Mailing List",
140
+ 'forum' => "Support Forum",
141
+ 'support' => "Support Forum",
142
+ 'faq' => "Fact Sheet",
143
+ 'home' => "Homepage",
144
+ 'homepage' => "Homepage",
145
+ 'web' => "Website",
146
+ 'website' => "Website",
147
+ 'blog' => "Blog",
148
+ 'pkg' => "Download",
149
+ 'dist' => "Download",
150
+ 'api' => "API Guide",
151
+ 'irc' => "IRC Channel",
152
+ 'chat' => "IRC Channel",
153
+ 'doc' => "Documentation",
154
+ 'docs' => "Documentation",
155
+ 'wiki' => "User Guide"
156
+ }
157
+
158
+ #
159
+ #
160
+ #
161
+ KNOWN_TYPES = TYPE_LABELS.keys
162
+
163
+ private
164
+
165
+ #
166
+ #
167
+ #
168
+ def infer_label(type)
169
+ TYPE_LABELS[type.to_s.downcase]
170
+ end
171
+
172
+ #
173
+ # @todo Deciding on a set of recognized types and inferences for them is rather tricky.
174
+ # Some sort of guiding design principle would be helpful.
175
+ #
176
+ def infer_type(string)
177
+ case string.to_s.downcase
178
+ when /work/i, /dev/i
179
+ 'dev' #'development'
180
+ when /code/i, /source/i
181
+ 'code' #'source-code'
182
+ when /bugs/i, /issue/i, /tracker/i
183
+ 'bugs' #'issue-tracker'
184
+ when /mail/i, /list/i
185
+ 'mail' #'mailing-list'
186
+ when /chat/i, /irc/i
187
+ 'chat' #'online-chat'
188
+ when /forum/i, /support/i, /q\&a/i
189
+ 'help' #'support-forum'
190
+ when /wiki/i, /user/i
191
+ 'wiki' #'wiki-wiki'
192
+ when /blog/i, /weblog/i
193
+ 'blog' #'weblog'
194
+ when /home/i, /website/i
195
+ 'home' #'homepage'
196
+ when /gem/i, /packge/i, /dist/i, /download/i
197
+ 'dist' #'package' or 'distribution' ?
198
+ when /api/i, /reference/i, /guide/i
199
+ 'api' #'reference' ???
200
+ when /doc/i
201
+ 'doc' #'documentation'
202
+ else
203
+ nil
204
+ end
205
+ end
206
+
207
+ end
208
+
209
+ end
@@ -0,0 +1,14 @@
1
+ module Indexer
2
+
3
+ # Conversion module provides routines for converting
4
+ # other metadata sources to and from index format.
5
+ #
6
+ module Conversion
7
+ end
8
+
9
+ end
10
+
11
+ require_relative 'conversion/gemspec'
12
+ require_relative 'conversion/gemspec_exporter'
13
+ require_relative 'conversion/gemfile'
14
+
@@ -0,0 +1,44 @@
1
+ module Indexer
2
+
3
+ module Conversion
4
+
5
+ #
6
+ # Update Spec with a Gemfile.
7
+ #
8
+ # @param [nil,String,::Bundler::DSL,::Bundler::Definition]
9
+ # Gemfile path, Bundler::Dsl or Bundler::Definition instance.
10
+ #
11
+ def import_gemfile(file=nil)
12
+ require 'bundler'
13
+
14
+ case file
15
+ when String
16
+ # FIXME: Is this the correct way fot load a gemfile?
17
+ bundle = ::Bundler::Dsl.new
18
+ bundle.eval_gemfile(file)
19
+ when NilClass
20
+ bundle = ::Bundler.definition
21
+ end
22
+
23
+ begin
24
+ merge!(bundle.metadata.to_h)
25
+ rescue
26
+ end
27
+
28
+ bundle.dependencies.each do |d|
29
+ add_requirement(d.name,
30
+ :version => d.requirement.to_s, # may need to parse this
31
+ :groups => d.groups,
32
+ :development => (d.type == :development)
33
+ #:engines => d.platforms ?
34
+ #:platforms => ?
35
+ )
36
+ end
37
+ end
38
+
39
+ alias_method :gemfile, :import_gemfile
40
+
41
+ end
42
+
43
+ end
44
+
@@ -0,0 +1,114 @@
1
+ module Indexer
2
+
3
+ module Conversion
4
+
5
+ #
6
+ # Create a Gem::Specification from Metadata.
7
+ #
8
+ # Becuase the specificaiton is extensive, a Gem::Specification
9
+ # can be created that is sufficient for most needs. However, there
10
+ # are a few points where the two do not intersect. In these cases
11
+ # the remaining gemspec fields need to be provided post conversion.
12
+ #
13
+ # Gem::Specification fields that the specification can't provide include:
14
+ #
15
+ # * platform
16
+ #
17
+ # In addition, some information can only be provided if a project's `root`
18
+ # directory is given. In these cases the most common project conventions
19
+ # are utilized in determining field values. Gem::Specification fields that
20
+ # draw from project files include:
21
+ #
22
+ # * files
23
+ # * bindir
24
+ # * extensions
25
+ # * rdoc_options
26
+ # * rdoc_extra_files
27
+ #
28
+ # Any or all of which can be reassigned post conversion, if need be.
29
+ #
30
+ # @param [Hash] options
31
+ # Convertion options.
32
+ #
33
+ # @option options [NilClass,String] :root
34
+ # project root directory
35
+ #
36
+ def to_gemspec(options={})
37
+ options[:data] = self.to_h
38
+ GemspecExporter.new(options).to_gemspec
39
+ end
40
+
41
+ #
42
+ # Import a Gem::Specification into Spec. This is intended to make it
43
+ # fairly easy to build the metadata from a pre-existing `.gemspec`.
44
+ #
45
+ # By making this an instance method, it is possible to import other
46
+ # resources into the Spec prior to a `.gemspec`.
47
+ #
48
+ # @todo Ensure all data possible is gathered from the gemspec.
49
+ #
50
+ def import_gemspec(gemspec)
51
+ require 'rubygems'
52
+
53
+ if not Gem::Specification === gemspec
54
+ # TODO: YAML-based gem specs ?
55
+ gemspec = Gem::Specification.load(gemspec)
56
+ end
57
+
58
+ # TODO: ensure this is robust
59
+ authors = (
60
+ zip = [gemspec.authors].flatten.zip([gemspec.email].flatten)
61
+ zip.map do |(name, email)|
62
+ email ? {:name=>name, :email=>email} : {:name=>name}
63
+ end
64
+ )
65
+
66
+ # TODO: how to handle license(s) ?
67
+
68
+ self.name = gemspec.name
69
+ self.version = gemspec.version.to_s
70
+ self.date = gemspec.date
71
+ self.title = gemspec.name.capitalize
72
+ self.summary = gemspec.summary
73
+ self.description = gemspec.description || gemspec.summary
74
+ self.authors = authors
75
+ self.load_path = gemspec.require_paths
76
+ self.homepage = gemspec.homepage
77
+
78
+ #self.engines = gemspec.platform
79
+ #self.extensions = gemspec.extensions
80
+
81
+ # TODO: Spec currently doesn't support multiple constraints for requirements.
82
+ # Probably 99.999% of the time it doesn't matter.
83
+ gemspec.dependencies.each do |d|
84
+ if d.type == :runtime
85
+ add_requirement(d.name, :versions=>d.requirements_list.first)
86
+ else
87
+ add_requirement(d.name, :versions=>d.requirements_list.first, :development=>true)
88
+ end
89
+ end
90
+ end
91
+
92
+ end
93
+
94
+ end
95
+
96
+
97
+ =begin
98
+
99
+ case gemspec
100
+ when ::Gem::Specification
101
+ spec = gemspec
102
+ else
103
+ file = Dir[root + "{*,}.gemspec"].first
104
+ return unless file
105
+
106
+ text = File.read(file)
107
+ if text =~ /\A---/
108
+ spec = ::Gem::Specification.from_yaml(text)
109
+ else
110
+ spec = ::Gem::Specification.load(file)
111
+ end
112
+ end
113
+
114
+ =end
@@ -0,0 +1,304 @@
1
+ # encoding: utf-8
2
+
3
+ require 'yaml'
4
+ require 'pathname'
5
+
6
+ module Indexer
7
+
8
+ # Convert index data into a gemspec.
9
+ #
10
+ # Notes:
11
+ # * Assumes all executables are in bin/.
12
+ # * Does not yet handle default_executable setting.
13
+ # * Does not yet handle platform setting.
14
+ # * Does not yet handle required_ruby_version.
15
+ # * Support for rdoc entries is weak.
16
+ #
17
+ class GemspecExporter
18
+
19
+ # File globs to include in package --unless a manifest file exists.
20
+ FILES = ".index .yardopts alt bin data demo ext features lib man spec test try* [A-Z]*.*" unless defined?(FILES)
21
+
22
+ # File globs to omit from FILES.
23
+ OMIT = "Config.rb" unless defined?(OMIT)
24
+
25
+ # Standard file patterns.
26
+ PATTERNS = {
27
+ :root => '{.index,Gemfile}',
28
+ :bin => 'bin/*',
29
+ :lib => 'lib/{**/}*', #.rb',
30
+ :ext => 'ext/{**/}extconf.rb',
31
+ :doc => '*.{txt,rdoc,md,markdown,tt,textile}',
32
+ :test => '{test,spec}/{**/}*.rb'
33
+ } unless defined?(PATTERNS)
34
+
35
+ # For which revision of indexer spec is this converter intended?
36
+ REVISION = 2013 unless defined?(REVISION)
37
+
38
+ #
39
+ def self.gemspec
40
+ new.to_gemspec
41
+ end
42
+
43
+ #
44
+ attr :metadata
45
+
46
+ #
47
+ def initialize(metadata=nil)
48
+ @root_check = false
49
+
50
+ if metadata
51
+ root_dir = metadata.delete(:root)
52
+ if root_dir
53
+ @root = root_dir
54
+ @root_check = true
55
+ end
56
+ metadata = nil if metadata.empty?
57
+ end
58
+
59
+ @metadata = metadata || YAML.load_file(root + '.index')
60
+
61
+ if @metadata['revision'].to_i != REVISION
62
+ warn "This gemspec exporter was not designed for this revision of index metadata."
63
+ end
64
+ end
65
+
66
+ #
67
+ def has_root?
68
+ root ? true : false
69
+ end
70
+
71
+ #
72
+ def root
73
+ return @root if @root || @root_check
74
+ @root_check = true
75
+ @root = find_root
76
+ end
77
+
78
+ #
79
+ def manifest
80
+ return nil unless root
81
+ @manifest ||= Dir.glob(root + 'manifest{,.txt}', File::FNM_CASEFOLD).first
82
+ end
83
+
84
+ #
85
+ def scm
86
+ return nil unless root
87
+ @scm ||= %w{git hg}.find{ |m| (root + ".#{m}").directory? }.to_sym
88
+ end
89
+
90
+ #
91
+ def files
92
+ return [] unless root
93
+ @files ||= \
94
+ if manifest
95
+ File.readlines(manifest).
96
+ map{ |line| line.strip }.
97
+ reject{ |line| line.empty? || line[0,1] == '#' }
98
+ else
99
+ list = []
100
+ Dir.chdir(root) do
101
+ FILES.split(/\s+/).each do |pattern|
102
+ list.concat(glob(pattern))
103
+ end
104
+ OMIT.split(/\s+/).each do |pattern|
105
+ list = list - glob(pattern)
106
+ end
107
+ end
108
+ list
109
+ end.select{ |path| File.file?(path) }.uniq
110
+ end
111
+
112
+ #
113
+ def glob_files(pattern)
114
+ return [] unless root
115
+ Dir.chdir(root) do
116
+ Dir.glob(pattern).select do |path|
117
+ File.file?(path) && files.include?(path)
118
+ end
119
+ end
120
+ end
121
+
122
+ def patterns
123
+ PATTERNS
124
+ end
125
+
126
+ def executables
127
+ @executables ||= \
128
+ glob_files(patterns[:bin]).map do |path|
129
+ File.basename(path)
130
+ end
131
+ end
132
+
133
+ def extensions
134
+ @extensions ||= \
135
+ glob_files(patterns[:ext]).map do |path|
136
+ File.basename(path)
137
+ end
138
+ end
139
+
140
+ def name
141
+ metadata['name'] || metadata['title'].downcase.gsub(/\W+/,'_')
142
+ end
143
+
144
+ def homepage
145
+ page = (
146
+ metadata['resources'].find{ |r| r['type'] =~ /^home/i } ||
147
+ metadata['resources'].find{ |r| r['name'] =~ /^home/i } ||
148
+ metadata['resources'].find{ |r| r['name'] =~ /^web/i }
149
+ )
150
+ page ? page['uri'] : false
151
+ end
152
+
153
+ def licenses
154
+ metadata['copyrights'].map{ |c| c['license'] }.compact
155
+ end
156
+
157
+ def require_paths
158
+ metadata['load_path'] || ['lib']
159
+ end
160
+
161
+ #
162
+ # Convert to gemnspec.
163
+ #
164
+ def to_gemspec
165
+ if has_root?
166
+ Gem::Specification.new do |gemspec|
167
+ to_gemspec_data(gemspec)
168
+ to_gemspec_paths(gemspec)
169
+ end
170
+ else
171
+ Gem::Specification.new do |gemspec|
172
+ to_gemspec_data(gemspec)
173
+ to_gemspec_paths(gemspec)
174
+ end
175
+ end
176
+ end
177
+
178
+ #
179
+ # Convert pure data settings.
180
+ #
181
+ def to_gemspec_data(gemspec)
182
+ gemspec.name = name
183
+ gemspec.version = metadata['version']
184
+ gemspec.summary = metadata['summary']
185
+ gemspec.description = metadata['description']
186
+
187
+ metadata['authors'].each do |author|
188
+ gemspec.authors << author['name']
189
+
190
+ if author.has_key?('email')
191
+ if gemspec.email
192
+ gemspec.email << author['email']
193
+ else
194
+ gemspec.email = [author['email']]
195
+ end
196
+ end
197
+ end
198
+
199
+ gemspec.licenses = licenses
200
+
201
+ requirements = metadata['requirements'] || []
202
+ requirements.each do |req|
203
+ next if req['optional']
204
+ next if req['external']
205
+
206
+ name = req['name']
207
+ groups = req['groups'] || []
208
+
209
+ version = gemify_version(req['version'])
210
+
211
+ if groups.empty? or groups.include?('runtime')
212
+ # populate runtime dependencies
213
+ if gemspec.respond_to?(:add_runtime_dependency)
214
+ gemspec.add_runtime_dependency(name,*version)
215
+ else
216
+ gemspec.add_dependency(name,*version)
217
+ end
218
+ else
219
+ # populate development dependencies
220
+ if gemspec.respond_to?(:add_development_dependency)
221
+ gemspec.add_development_dependency(name,*version)
222
+ else
223
+ gemspec.add_dependency(name,*version)
224
+ end
225
+ end
226
+ end
227
+
228
+ # convert external dependencies into gemspec requirements
229
+ requirements.each do |req|
230
+ next unless req['external']
231
+ gemspec.requirements << ("%s-%s" % req.values_at('name', 'version'))
232
+ end
233
+
234
+ gemspec.homepage = homepage
235
+ gemspec.require_paths = require_paths
236
+ gemspec.post_install_message = metadata['install_message']
237
+ end
238
+
239
+ #
240
+ # Set gemspec settings that require a root directory path.
241
+ #
242
+ def to_gemspec_paths(gemspec)
243
+ gemspec.files = files
244
+ gemspec.extensions = extensions
245
+ gemspec.executables = executables
246
+
247
+ if Gem::VERSION < '1.7.'
248
+ gemspec.default_executable = gemspec.executables.first
249
+ end
250
+
251
+ gemspec.test_files = glob_files(patterns[:test])
252
+
253
+ unless gemspec.files.include?('.document')
254
+ gemspec.extra_rdoc_files = glob_files(patterns[:doc])
255
+ end
256
+ end
257
+
258
+ #
259
+ # Return a copy of this file. This is used to generate a local
260
+ # .gemspec file that can automatically read the index file.
261
+ #
262
+ def self.source_code
263
+ File.read(__FILE__)
264
+ end
265
+
266
+ private
267
+
268
+ def find_root
269
+ root_files = patterns[:root]
270
+ if Dir.glob(root_files).first
271
+ Pathname.new(Dir.pwd)
272
+ elsif Dir.glob("../#{root_files}").first
273
+ Pathname.new(Dir.pwd).parent
274
+ else
275
+ #raise "Can't find root of project containing `#{root_files}'."
276
+ warn "Can't find root of project containing `#{root_files}'."
277
+ nil
278
+ end
279
+ end
280
+
281
+ def glob(pattern)
282
+ if File.directory?(pattern)
283
+ Dir.glob(File.join(pattern, '**', '*'))
284
+ else
285
+ Dir.glob(pattern)
286
+ end
287
+ end
288
+
289
+ def gemify_version(version)
290
+ case version
291
+ when /^(.*?)\+$/
292
+ ">= #{$1}"
293
+ when /^(.*?)\-$/
294
+ "< #{$1}"
295
+ when /^(.*?)\~$/
296
+ "~> #{$1}"
297
+ else
298
+ version
299
+ end
300
+ end
301
+
302
+ end
303
+
304
+ end