indexer 0.1.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.
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