puppet_forge 1.0.6 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/CHANGELOG.md +23 -0
  2. data/MAINTAINERS +13 -0
  3. data/README.md +48 -6
  4. data/lib/puppet_forge.rb +4 -0
  5. data/lib/puppet_forge/connection.rb +81 -0
  6. data/lib/puppet_forge/connection/connection_failure.rb +26 -0
  7. data/lib/puppet_forge/error.rb +34 -0
  8. data/lib/{her → puppet_forge}/lazy_accessors.rb +20 -27
  9. data/lib/{her → puppet_forge}/lazy_relations.rb +28 -9
  10. data/lib/puppet_forge/middleware/symbolify_json.rb +72 -0
  11. data/lib/puppet_forge/tar.rb +10 -0
  12. data/lib/puppet_forge/tar/mini.rb +81 -0
  13. data/lib/puppet_forge/unpacker.rb +68 -0
  14. data/lib/puppet_forge/v3.rb +11 -0
  15. data/lib/puppet_forge/v3/base.rb +106 -73
  16. data/lib/puppet_forge/v3/base/paginated_collection.rb +23 -14
  17. data/lib/puppet_forge/v3/metadata.rb +197 -0
  18. data/lib/puppet_forge/v3/module.rb +2 -1
  19. data/lib/puppet_forge/v3/release.rb +33 -8
  20. data/lib/puppet_forge/v3/user.rb +2 -0
  21. data/lib/puppet_forge/version.rb +1 -1
  22. data/puppet_forge.gemspec +6 -3
  23. data/spec/fixtures/v3/modules/puppetlabs-apache.json +21 -1
  24. data/spec/fixtures/v3/releases/puppetlabs-apache-0.0.1.json +4 -1
  25. data/spec/integration/forge/v3/module_spec.rb +79 -0
  26. data/spec/integration/forge/v3/release_spec.rb +75 -0
  27. data/spec/integration/forge/v3/user_spec.rb +70 -0
  28. data/spec/spec_helper.rb +15 -8
  29. data/spec/unit/forge/connection/connection_failure_spec.rb +30 -0
  30. data/spec/unit/forge/connection_spec.rb +53 -0
  31. data/spec/unit/{her → forge}/lazy_accessors_spec.rb +20 -13
  32. data/spec/unit/{her → forge}/lazy_relations_spec.rb +60 -46
  33. data/spec/unit/forge/middleware/symbolify_json_spec.rb +63 -0
  34. data/spec/unit/forge/tar/mini_spec.rb +85 -0
  35. data/spec/unit/forge/tar_spec.rb +9 -0
  36. data/spec/unit/forge/unpacker_spec.rb +58 -0
  37. data/spec/unit/forge/v3/base/paginated_collection_spec.rb +68 -46
  38. data/spec/unit/forge/v3/base_spec.rb +1 -1
  39. data/spec/unit/forge/v3/metadata_spec.rb +300 -0
  40. data/spec/unit/forge/v3/module_spec.rb +14 -36
  41. data/spec/unit/forge/v3/release_spec.rb +9 -30
  42. data/spec/unit/forge/v3/user_spec.rb +7 -7
  43. metadata +127 -41
  44. checksums.yaml +0 -7
  45. data/lib/puppet_forge/middleware/json_for_her.rb +0 -37
@@ -2,22 +2,31 @@ module PuppetForge
2
2
  module V3
3
3
  class Base
4
4
 
5
- # A subclass of Her::Collection that enables navigation of the Forge API's
6
- # paginated datasets.
7
- class PaginatedCollection < Her::Collection
5
+ # Enables navigation of the Forge API's paginated datasets.
6
+ class PaginatedCollection < Array
7
+
8
+ # Default pagination limit for API request
9
+ LIMIT = 20
8
10
 
9
- # In addition to the standard Her::Collection arguments, this
10
- # constructor requires a reference to the paginated class. This enables
11
- # the collection to load related pages.
12
- #
13
11
  # @api private
14
- # @param klass [Her::Model] the class to page over
12
+ # @param klass [PuppetForge::V3::Base] the class to page over
15
13
  # @param data [Array] the current data page
16
14
  # @param metadata [Hash<(:limit, :total, :offset)>] page metadata
17
15
  # @param errors [Object] errors for the page request
18
- def initialize(klass, data, metadata, errors)
19
- super(data, metadata, errors)
16
+ def initialize(klass, data = [], metadata = {:total => 0, :offset => 0, :limit => LIMIT}, errors = nil)
17
+ super()
18
+ @metadata = metadata
19
+ @errors = errors
20
20
  @klass = klass
21
+
22
+ data.each do |item|
23
+ self << @klass.new(item)
24
+ end
25
+ end
26
+
27
+ # For backwards compatibility, all returns the current object.
28
+ def all
29
+ self
21
30
  end
22
31
 
23
32
  # An enumerator that iterates over the entire collection, independent
@@ -26,7 +35,7 @@ module PuppetForge
26
35
  #
27
36
  # @return [Enumerator] an iterator for the entire collection
28
37
  def unpaginated
29
- page = @klass.get_collection(metadata[:first])
38
+ page = @klass.get_collection(@metadata[:first])
30
39
  Enumerator.new do |emitter|
31
40
  loop do
32
41
  page.each { |x| emitter << x }
@@ -42,7 +51,7 @@ module PuppetForge
42
51
  # @!method offset
43
52
  # @return [Integer] the offset for the current page
44
53
  [ :total, :limit, :offset ].each do |info|
45
- define_method(info) { metadata[info] }
54
+ define_method(info) { @metadata[info] }
46
55
  end
47
56
 
48
57
  [ :next, :previous ].each do |link|
@@ -53,7 +62,7 @@ module PuppetForge
53
62
  # Returns the previous page if a previous page exists.
54
63
  # @return [PaginatedCollection, nil] the previous page
55
64
  define_method(link) do
56
- return unless path = metadata[link]
65
+ return unless path = @metadata[link]
57
66
  @klass.get_collection(path)
58
67
  end
59
68
 
@@ -64,7 +73,7 @@ module PuppetForge
64
73
  # Returns the url of the previous page if a previous page exists.
65
74
  # @return [String, nil] the previous page's url
66
75
  define_method("#{link}_url") do
67
- metadata[link]
76
+ @metadata[link]
68
77
  end
69
78
  end
70
79
  end
@@ -0,0 +1,197 @@
1
+ require 'uri'
2
+ require 'json'
3
+ require 'set'
4
+ require 'semantic_puppet/version'
5
+
6
+ module PuppetForge
7
+ module V3
8
+ # This class provides a data structure representing a module's metadata.
9
+ # @api private
10
+ class Metadata
11
+
12
+ attr_accessor :module_name
13
+
14
+ DEFAULTS = {
15
+ 'name' => nil,
16
+ 'version' => nil,
17
+ 'author' => nil,
18
+ 'summary' => nil,
19
+ 'license' => 'Apache-2.0',
20
+ 'source' => '',
21
+ 'project_page' => nil,
22
+ 'issues_url' => nil,
23
+ 'dependencies' => Set.new.freeze,
24
+ }
25
+
26
+ def initialize
27
+ @data = DEFAULTS.dup
28
+ @data['dependencies'] = @data['dependencies'].dup
29
+ end
30
+
31
+ # Returns a filesystem-friendly version of this module name.
32
+ def dashed_name
33
+ PuppetForge::V3.normalize_name(@data['name']) if @data['name']
34
+ end
35
+
36
+ # Returns a string that uniquely represents this version of this module.
37
+ def release_name
38
+ return nil unless @data['name'] && @data['version']
39
+ [ dashed_name, @data['version'] ].join('-')
40
+ end
41
+
42
+ alias :name :module_name
43
+ alias :full_module_name :dashed_name
44
+
45
+ # Merges the current set of metadata with another metadata hash. This
46
+ # method also handles the validation of module names and versions, in an
47
+ # effort to be proactive about module publishing constraints.
48
+ def update(data, with_dependencies = true)
49
+ process_name(data) if data['name']
50
+ process_version(data) if data['version']
51
+ process_source(data) if data['source']
52
+ merge_dependencies(data) if with_dependencies && data['dependencies']
53
+
54
+ @data.merge!(data)
55
+ return self
56
+ end
57
+
58
+ # Validates the name and version_requirement for a dependency, then creates
59
+ # the Dependency and adds it.
60
+ # Returns the Dependency that was added.
61
+ def add_dependency(name, version_requirement=nil, repository=nil)
62
+ validate_name(name)
63
+ validate_version_range(version_requirement) if version_requirement
64
+
65
+ if dup = @data['dependencies'].find { |d| d.full_module_name == name && d.version_requirement != version_requirement }
66
+ raise ArgumentError, "Dependency conflict for #{full_module_name}: Dependency #{name} was given conflicting version requirements #{version_requirement} and #{dup.version_requirement}. Verify that there are no duplicates in the metadata.json or the Modulefile."
67
+ end
68
+
69
+ dep = Dependency.new(name, version_requirement, repository)
70
+ @data['dependencies'].add(dep)
71
+
72
+ dep
73
+ end
74
+
75
+ # Provides an accessor for the now defunct 'description' property. This
76
+ # addresses a regression in Puppet 3.6.x where previously valid templates
77
+ # refering to the 'description' property were broken.
78
+ # @deprecated
79
+ def description
80
+ @data['description']
81
+ end
82
+
83
+ def dependencies
84
+ @data['dependencies'].to_a
85
+ end
86
+
87
+ # Returns a hash of the module's metadata. Used by Puppet's automated
88
+ # serialization routines.
89
+ #
90
+ # @see Puppet::Network::FormatSupport#to_data_hash
91
+ def to_hash
92
+ @data
93
+ end
94
+ alias :to_data_hash :to_hash
95
+
96
+ def to_json
97
+ data = @data.dup.merge('dependencies' => dependencies)
98
+
99
+ contents = data.keys.map do |k|
100
+ value = (JSON.pretty_generate(data[k]) rescue data[k].to_json)
101
+ "#{k.to_json}: #{value}"
102
+ end
103
+
104
+ "{\n" + contents.join(",\n").gsub(/^/, ' ') + "\n}\n"
105
+ end
106
+
107
+ # Expose any metadata keys as callable reader methods.
108
+ def method_missing(name, *args)
109
+ return @data[name.to_s] if @data.key? name.to_s
110
+ super
111
+ end
112
+
113
+ private
114
+
115
+ # Do basic validation and parsing of the name parameter.
116
+ def process_name(data)
117
+ validate_name(data['name'])
118
+ author, @module_name = data['name'].split(/[-\/]/, 2)
119
+
120
+ data['author'] ||= author if @data['author'] == DEFAULTS['author']
121
+ end
122
+
123
+ # Do basic validation on the version parameter.
124
+ def process_version(data)
125
+ validate_version(data['version'])
126
+ end
127
+
128
+ # Do basic parsing of the source parameter. If the source is hosted on
129
+ # GitHub, we can predict sensible defaults for both project_page and
130
+ # issues_url.
131
+ def process_source(data)
132
+ if data['source'] =~ %r[://]
133
+ source_uri = URI.parse(data['source'])
134
+ else
135
+ source_uri = URI.parse("http://#{data['source']}")
136
+ end
137
+
138
+ if source_uri.host =~ /^(www\.)?github\.com$/
139
+ source_uri.scheme = 'https'
140
+ source_uri.path.sub!(/\.git$/, '')
141
+ data['project_page'] ||= @data['project_page'] || source_uri.to_s
142
+ data['issues_url'] ||= @data['issues_url'] || source_uri.to_s.sub(/\/*$/, '') + '/issues'
143
+ end
144
+
145
+ rescue URI::Error
146
+ return
147
+ end
148
+
149
+ # Validates and parses the dependencies.
150
+ def merge_dependencies(data)
151
+ data['dependencies'].each do |dep|
152
+ add_dependency(dep['name'], dep['version_requirement'], dep['repository'])
153
+ end
154
+
155
+ # Clear dependencies so @data dependencies are not overwritten
156
+ data.delete 'dependencies'
157
+ end
158
+
159
+ # Validates that the given module name is both namespaced and well-formed.
160
+ def validate_name(name)
161
+ return if name =~ /\A[a-z0-9]+[-\/][a-z][a-z0-9_]*\Z/i
162
+
163
+ namespace, modname = name.split(/[-\/]/, 2)
164
+ modname = :namespace_missing if namespace == ''
165
+
166
+ err = case modname
167
+ when nil, '', :namespace_missing
168
+ "the field must be a namespaced module name"
169
+ when /[^a-z0-9_]/i
170
+ "the module name contains non-alphanumeric (or underscore) characters"
171
+ when /^[^a-z]/i
172
+ "the module name must begin with a letter"
173
+ else
174
+ "the namespace contains non-alphanumeric characters"
175
+ end
176
+
177
+ raise ArgumentError, "Invalid 'name' field in metadata.json: #{err}"
178
+ end
179
+
180
+ # Validates that the version string can be parsed by SemanticPuppet.
181
+ def validate_version(version)
182
+ return if SemanticPuppet::Version.valid?(version)
183
+
184
+ err = "version string cannot be parsed as a valid Semantic Version"
185
+ raise ArgumentError, "Invalid 'version' field in metadata.json: #{err}"
186
+ end
187
+
188
+ # Validates that the version range can be parsed by SemanticPuppet.
189
+ def validate_version_range(version_range)
190
+ SemanticPuppet::VersionRange.parse(version_range)
191
+ rescue ArgumentError => e
192
+ raise ArgumentError, "Invalid 'version_range' field in metadata.json: #{e}"
193
+ end
194
+ end
195
+ end
196
+ end
197
+
@@ -9,7 +9,8 @@ module PuppetForge
9
9
  class Module < Base
10
10
  lazy :owner, 'User'
11
11
  lazy :current_release, 'Release'
12
- lazy_collection :releases
12
+ lazy_collection :releases, 'Release'
13
+
13
14
  end
14
15
  end
15
16
  end
@@ -6,8 +6,8 @@ module PuppetForge
6
6
 
7
7
  # Models a specific release version of a Puppet Module on the Forge.
8
8
  class Release < Base
9
- lazy :module
10
-
9
+ lazy :module, 'Module'
10
+
11
11
  # Returns a fully qualified URL for downloading this release from the Forge.
12
12
  #
13
13
  # @return [String] fully qualified download URL for release
@@ -21,15 +21,40 @@ module PuppetForge
21
21
 
22
22
  # Downloads the Release tarball to the specified file path.
23
23
  #
24
- # @todo Stream the tarball data to disk.
25
- # @param file [String] the file to create
24
+ # @param path [Pathname]
26
25
  # @return [void]
27
- def download(file)
28
- self.class.get_raw(download_url)[:response].on_complete do |env|
29
- File.open(file, 'wb') { |file| file.write(env[:body]) }
26
+ def download(path)
27
+ resp = self.class.conn.get(file_url)
28
+ path.open('wb') { |fh| fh.write(resp.body) }
29
+ rescue Faraday::ResourceNotFound => e
30
+ raise PuppetForge::ReleaseNotFound, "The module release #{slug} does not exist on #{conn.url_prefix}.", e.backtrace
31
+ end
32
+
33
+ # Verify that a downloaded module matches the checksum in the metadata for this release.
34
+ #
35
+ # @param path [Pathname]
36
+ # @return [void]
37
+ def verify(path)
38
+ expected_md5 = file_md5
39
+ file_md5 = Digest::MD5.file(path).hexdigest
40
+ if expected_md5 != file_md5
41
+ raise ChecksumMismatch.new("Expected #{path} checksum to be #{expected_md5}, got #{file_md5}")
30
42
  end
31
- nil
32
43
  end
44
+
45
+ private
46
+
47
+ def file_url
48
+ "/v3/files/#{slug}.tar.gz"
49
+ end
50
+
51
+ def resource_url
52
+ "/v3/releases/#{slug}"
53
+ end
54
+
55
+ class ChecksumMismatch < StandardError
56
+ end
57
+
33
58
  end
34
59
  end
35
60
  end
@@ -7,6 +7,8 @@ module PuppetForge
7
7
  # Models a Forge user's account.
8
8
  class User < Base
9
9
 
10
+ include PuppetForge::LazyAccessors
11
+
10
12
  # Returns a collection of Modules owned by the user.
11
13
  #
12
14
  # @note Because there is no related module data in the record, we can't
@@ -1,3 +1,3 @@
1
1
  module PuppetForge
2
- VERSION = '1.0.6' # Library version
2
+ VERSION = '2.0.0' # Library version
3
3
  end
@@ -8,7 +8,8 @@ Gem::Specification.new do |spec|
8
8
  spec.version = PuppetForge::VERSION
9
9
  spec.authors = ["Puppet Labs"]
10
10
  spec.email = ["forge-team+api@puppetlabs.com"]
11
- spec.summary = "Access and manipulate the Puppet Forge API from Ruby."
11
+ spec.summary = "Access the Puppet Forge API from Ruby for resource information and to download releases."
12
+ spec.description = %q{Tools that can be used to access Forge API information on Modules, Users, and Releases. As well as download, unpack, and install Releases to a directory.}
12
13
  spec.homepage = "https://github.com/puppetlabs/forge-ruby"
13
14
  spec.license = "Apache-2.0"
14
15
 
@@ -19,8 +20,10 @@ Gem::Specification.new do |spec|
19
20
 
20
21
  spec.required_ruby_version = '>= 1.9.3'
21
22
 
22
- spec.add_runtime_dependency "activesupport", "~> 4.2"
23
- spec.add_runtime_dependency "her", "~> 0.6.8"
23
+ spec.add_runtime_dependency "faraday", "~> 0.9.0"
24
+ spec.add_runtime_dependency "faraday_middleware", "~> 0.9.0"
25
+ spec.add_dependency 'semantic_puppet', '~> 0.1.0'
26
+ spec.add_dependency 'minitar'
24
27
 
25
28
  spec.add_development_dependency "bundler", "~> 1.6"
26
29
  spec.add_development_dependency "rake"
@@ -1,16 +1,19 @@
1
1
  {
2
2
  "uri": "/v3/modules/puppetlabs-apache",
3
+ "slug": "puppetlabs-apache",
3
4
  "name": "apache",
4
5
  "downloads": 81387,
5
6
  "created_at": "2010-05-20 22:43:19 -0700",
6
7
  "updated_at": "2014-01-06 14:42:07 -0800",
7
8
  "owner": {
8
9
  "uri": "/v3/users/puppetlabs",
10
+ "slug": "puppetlabs",
9
11
  "username": "puppetlabs",
10
12
  "gravatar_id": "fdd009b7c1ec96e088b389f773e87aec"
11
13
  },
12
14
  "current_release": {
13
15
  "uri": "/v3/releases/puppetlabs-apache-0.10.0",
16
+ "slug": "puppetlabs-apache-0.10.0",
14
17
  "module": {
15
18
  "uri": "/v3/modules/puppetlabs-apache",
16
19
  "name": "apache",
@@ -318,73 +321,90 @@
318
321
  "releases": [
319
322
  {
320
323
  "uri": "/v3/releases/puppetlabs-apache-0.10.0",
324
+ "slug": "puppetlabs-apache-0.10.0",
321
325
  "version": "0.10.0"
322
326
  },
323
327
  {
324
328
  "uri": "/v3/releases/puppetlabs-apache-0.9.0",
329
+ "slug": "puppetlabs-apache-0.9.0",
325
330
  "version": "0.9.0"
326
331
  },
327
332
  {
328
333
  "uri": "/v3/releases/puppetlabs-apache-0.8.1",
334
+ "slug": "puppetlabs-apache-0.8.1",
329
335
  "version": "0.8.1"
330
336
  },
331
337
  {
332
338
  "uri": "/v3/releases/puppetlabs-apache-0.8.0",
339
+ "slug": "puppetlabs-apache-0.8.0",
333
340
  "version": "0.8.0"
334
341
  },
335
342
  {
336
343
  "uri": "/v3/releases/puppetlabs-apache-0.7.0",
344
+ "slug": "puppetlabs-apache-0.7.0",
337
345
  "version": "0.7.0"
338
346
  },
339
347
  {
340
348
  "uri": "/v3/releases/puppetlabs-apache-0.6.0",
349
+ "slug": "puppetlabs-apache-0.6.0",
341
350
  "version": "0.6.0"
342
351
  },
343
352
  {
344
353
  "uri": "/v3/releases/puppetlabs-apache-0.5.0-rc1",
354
+ "slug": "puppetlabs-apache-0.5.0-rc1",
345
355
  "version": "0.5.0-rc1"
346
356
  },
347
357
  {
348
358
  "uri": "/v3/releases/puppetlabs-apache-0.4.0",
359
+ "slug": "puppetlabs-apache-0.4.0",
349
360
  "version": "0.4.0"
350
361
  },
351
362
  {
352
363
  "uri": "/v3/releases/puppetlabs-apache-0.3.0",
364
+ "slug": "puppetlabs-apache-0.3.0",
353
365
  "version": "0.3.0"
354
366
  },
355
367
  {
356
368
  "uri": "/v3/releases/puppetlabs-apache-0.2.2",
369
+ "slug": "puppetlabs-apache-0.2.2",
357
370
  "version": "0.2.2"
358
371
  },
359
372
  {
360
373
  "uri": "/v3/releases/puppetlabs-apache-0.2.1",
374
+ "slug": "puppetlabs-apache-0.2.1",
361
375
  "version": "0.2.1"
362
376
  },
363
377
  {
364
378
  "uri": "/v3/releases/puppetlabs-apache-0.2.0",
379
+ "slug": "puppetlabs-apache-0.2.0",
365
380
  "version": "0.2.0"
366
381
  },
367
382
  {
368
383
  "uri": "/v3/releases/puppetlabs-apache-0.1.1",
384
+ "slug": "puppetlabs-apache-0.1.1",
369
385
  "version": "0.1.1"
370
386
  },
371
387
  {
372
388
  "uri": "/v3/releases/puppetlabs-apache-0.0.4",
389
+ "slug": "puppetlabs-apache-0.0.4",
373
390
  "version": "0.0.4"
374
391
  },
375
392
  {
376
393
  "uri": "/v3/releases/puppetlabs-apache-0.0.3",
394
+ "slug": "puppetlabs-apache-0.0.3",
377
395
  "version": "0.0.3"
378
396
  },
379
397
  {
380
398
  "uri": "/v3/releases/puppetlabs-apache-0.0.2",
399
+ "slug": "puppetlabs-apache-0.0.2",
381
400
  "version": "0.0.2"
382
401
  },
383
402
  {
384
403
  "uri": "/v3/releases/puppetlabs-apache-0.0.1",
404
+ "slug": "puppetlabs-apache-0.0.1",
385
405
  "version": "0.0.1"
386
406
  }
387
407
  ],
388
408
  "homepage_url": "https://github.com/puppetlabs/puppetlabs-apache",
389
409
  "issues_url": "https://tickets.puppetlabs.com"
390
- }
410
+ }