puppet_forge 1.0.6 → 2.0.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 (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
+ }