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
@@ -0,0 +1,72 @@
1
+ module PuppetForge
2
+ module Middleware
3
+
4
+ # SymbolifyJson is a Faraday Middleware that will process any response formatted as a hash
5
+ # and change all the keys into symbols (as long as they respond to the method #to_sym.
6
+ #
7
+ # This middleware makes no changes to the values of the hash.
8
+ # If the response is not a hash, no changes will be made.
9
+ class SymbolifyJson < Faraday::Middleware
10
+
11
+ # Processes an array
12
+ #
13
+ # @return an array with any hash's keys turned into symbols if possible
14
+ def process_array(array)
15
+ array.map do |arg|
16
+ # Search any arrays and hashes for hash keys
17
+ if arg.is_a? Hash
18
+ process_hash(arg)
19
+ elsif arg.is_a? Array
20
+ process_array(arg)
21
+ else
22
+ arg
23
+ end
24
+ end
25
+ end
26
+
27
+ # Processes a hash
28
+ #
29
+ # @return a hash with all keys turned into symbols if possible
30
+ def process_hash(hash)
31
+
32
+ # hash.map returns an array in the format
33
+ # [ [key, value], [key2, value2], ... ]
34
+ # Hash[] converts that into a hash in the format
35
+ # { key => value, key2 => value2, ... }
36
+ Hash[hash.map do |key, val|
37
+ # Convert to a symbol if possible
38
+ if key.respond_to? :to_sym
39
+ new_key = key.to_sym
40
+ else
41
+ new_key = key
42
+ end
43
+
44
+ # If value is a hash or array look for more hash keys inside.
45
+ if val.is_a?(Hash)
46
+ [new_key, process_hash(val)]
47
+ elsif val.is_a?(Array)
48
+ [new_key, process_array(val)]
49
+ else
50
+ [new_key, val]
51
+ end
52
+ end]
53
+ end
54
+
55
+ def process_response(env)
56
+ if !env["body"].nil? && env["body"].is_a?(Hash)
57
+ process_hash(env.body)
58
+ else
59
+ env.body
60
+ end
61
+ end
62
+
63
+ def call(environment)
64
+ @app.call(environment).on_complete do |env|
65
+ env.body = process_response(env)
66
+ end
67
+ end
68
+
69
+ end
70
+ end
71
+ end
72
+
@@ -0,0 +1,10 @@
1
+
2
+ module PuppetForge
3
+ class Tar
4
+ require 'puppet_forge/tar/mini'
5
+
6
+ def self.instance
7
+ Mini.new
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,81 @@
1
+ require 'zlib'
2
+ require 'archive/tar/minitar'
3
+
4
+ module PuppetForge
5
+ class Tar
6
+ class Mini
7
+
8
+ SYMLINK_FLAGS = [2]
9
+ VALID_TAR_FLAGS = (0..7)
10
+
11
+ # @return [Hash{:symbol => Array<String>}] a hash with file-category keys pointing to lists of filenames.
12
+ def unpack(sourcefile, destdir)
13
+ # directories need to be changed outside of the Minitar::unpack because directories don't have a :file_done action
14
+ dirlist = []
15
+ file_lists = {}
16
+ Zlib::GzipReader.open(sourcefile) do |reader|
17
+ file_lists = validate_files(reader)
18
+ Archive::Tar::Minitar.unpack(reader, destdir, file_lists[:valid]) do |action, name, stats|
19
+ case action
20
+ when :file_done
21
+ FileUtils.chmod('u+rw,g+r,a-st', "#{destdir}/#{name}")
22
+ when :file_start
23
+ validate_entry(destdir, name)
24
+ when :dir
25
+ validate_entry(destdir, name)
26
+ dirlist << "#{destdir}/#{name}"
27
+ end
28
+ end
29
+ end
30
+ dirlist.each {|d| File.chmod(0755, d)}
31
+ file_lists
32
+ end
33
+
34
+ def pack(sourcedir, destfile)
35
+ Zlib::GzipWriter.open(destfile) do |writer|
36
+ Archive::Tar::Minitar.pack(sourcedir, writer)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ # Categorize all the files in tarfile as :valid, :invalid, or :symlink.
43
+ #
44
+ # :invalid files include 'x' and 'g' flags from the PAX standard but and any other non-standard tar flags.
45
+ # tar format info: http://pic.dhe.ibm.com/infocenter/zos/v1r13/index.jsp?topic=%2Fcom.ibm.zos.r13.bpxa500%2Ftaf.htm
46
+ # pax format info: http://pic.dhe.ibm.com/infocenter/zos/v1r13/index.jsp?topic=%2Fcom.ibm.zos.r13.bpxa500%2Fpxarchfm.htm
47
+ # :symlinks are not supported in Puppet modules
48
+ # :valid files are any of those that can be used in modules
49
+ # @param tarfile name of the tarfile
50
+ # @return [Hash{:symbol => Array<String>}] a hash with file-category keys pointing to lists of filenames.
51
+ def validate_files(tarfile)
52
+ file_lists = {:valid => [], :invalid => [], :symlinks => []}
53
+ Archive::Tar::Minitar.open(tarfile).each do |entry|
54
+ flag = entry.typeflag
55
+ if flag.nil? || flag =~ /[[:digit:]]/ && SYMLINK_FLAGS.include?(flag.to_i)
56
+ file_lists[:symlinks] << entry.name
57
+ elsif flag.nil? || flag =~ /[[:digit:]]/ && VALID_TAR_FLAGS.include?(flag.to_i)
58
+ file_lists[:valid] << entry.name
59
+ else
60
+ file_lists[:invalid] << entry.name
61
+ end
62
+ end
63
+ file_lists
64
+ end
65
+
66
+ def validate_entry(destdir, path)
67
+ if Pathname.new(path).absolute?
68
+ raise PuppetForge::InvalidPathInPackageError, :entry_path => path, :directory => destdir
69
+ end
70
+
71
+ path = File.expand_path File.join(destdir, path)
72
+
73
+ if path !~ /\A#{Regexp.escape destdir}/
74
+ raise PuppetForge::InvalidPathInPackageError, :entry_path => path, :directory => destdir
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ end
81
+
@@ -0,0 +1,68 @@
1
+ require 'pathname'
2
+ require 'puppet_forge/error'
3
+ require 'puppet_forge/tar'
4
+
5
+ module PuppetForge
6
+ class Unpacker
7
+ # Unpack a tar file into a specified directory
8
+ #
9
+ # @param filename [String] the file to unpack
10
+ # @param target [String] the target directory to unpack into
11
+ # @return [Hash{:symbol => Array<String>}] a hash with file-category keys pointing to lists of filenames.
12
+ # The categories are :valid, :invalid and :symlink
13
+ def self.unpack(filename, target, tmpdir)
14
+ inst = self.new(filename, target, tmpdir)
15
+ file_lists = inst.unpack
16
+ inst.move_into(Pathname.new(target))
17
+ file_lists
18
+ end
19
+
20
+ # Set the owner/group of the target directory to those of the source
21
+ # Note: don't call this function on Microsoft Windows
22
+ #
23
+ # @param source [Pathname] source of the permissions
24
+ # @param target [Pathname] target of the permissions change
25
+ def self.harmonize_ownership(source, target)
26
+ FileUtils.chown_R(source.stat.uid, source.stat.gid, target)
27
+ end
28
+
29
+ # @param filename [String] the file to unpack
30
+ # @param target [String] the target directory to unpack into
31
+ def initialize(filename, target, tmpdir)
32
+ @filename = filename
33
+ @target = target
34
+ @tmpdir = tmpdir
35
+ end
36
+
37
+ # @api private
38
+ def unpack
39
+ begin
40
+ PuppetForge::Tar.instance.unpack(@filename, @tmpdir)
41
+ rescue PuppetForge::ExecutionFailure => e
42
+ raise RuntimeError, "Could not extract contents of module archive: #{e.message}"
43
+ end
44
+ end
45
+
46
+ # @api private
47
+ def move_into(dir)
48
+ dir.rmtree if dir.exist?
49
+ FileUtils.mv(root_dir, dir)
50
+ ensure
51
+ FileUtils.rmtree(@tmpdir)
52
+ end
53
+
54
+ # @api private
55
+ def root_dir
56
+ return @root_dir if @root_dir
57
+
58
+ # Grab the first directory containing a metadata.json file
59
+ metadata_file = Dir["#{@tmpdir}/**/metadata.json"].sort_by(&:length)[0]
60
+
61
+ if metadata_file
62
+ @root_dir = Pathname.new(metadata_file).dirname
63
+ else
64
+ raise "No valid metadata.json found!"
65
+ end
66
+ end
67
+ end
68
+ end
@@ -2,9 +2,20 @@ module PuppetForge
2
2
 
3
3
  # Models specific to the Puppet Forge's v3 API.
4
4
  module V3
5
+ # Normalize a module name to use a hyphen as the separator between the
6
+ # author and module.
7
+
8
+ # @example
9
+ # PuppetForge::V3.normalize_name('my/module') #=> 'my-module'
10
+ # PuppetForge::V3.normalize_name('my-module') #=> 'my-module'
11
+ def self.normalize_name(name)
12
+ name.tr('/', '-')
13
+ end
5
14
  end
6
15
  end
7
16
 
17
+ require 'puppet_forge/v3/metadata'
18
+
8
19
  require 'puppet_forge/v3/user'
9
20
  require 'puppet_forge/v3/module'
10
21
  require 'puppet_forge/v3/release'
@@ -1,97 +1,130 @@
1
- require 'her'
2
- require 'her/lazy_accessors'
3
- require 'her/lazy_relations'
4
-
5
- require 'puppet_forge/middleware/json_for_her'
1
+ require 'puppet_forge/connection'
6
2
  require 'puppet_forge/v3/base/paginated_collection'
3
+ require 'puppet_forge/error'
4
+
5
+ require 'puppet_forge/lazy_accessors'
6
+ require 'puppet_forge/lazy_relations'
7
7
 
8
8
  module PuppetForge
9
9
  module V3
10
10
 
11
- # Acts as the base class for all PuppetForge::V3::* models. This class provides
12
- # some overrides of behaviors from Her, in addition to convenience methods
13
- # and abstractions of common behavior.
11
+ # Acts as the base class for all PuppetForge::V3::* models.
14
12
  #
15
13
  # @api private
16
14
  class Base
17
- include Her::Model
18
- include Her::LazyAccessors
19
- include Her::LazyRelations
20
-
21
- use_api begin
22
- begin
23
- # Use Typhoeus if available.
24
- Gem::Specification.find_by_name('typhoeus', '~> 0.6')
25
- require 'typhoeus/adapters/faraday'
26
- adapter = Faraday::Adapter::Typhoeus
27
- rescue Gem::LoadError
28
- adapter = Faraday::Adapter::NetHttp
29
- end
15
+ include PuppetForge::LazyAccessors
16
+ include PuppetForge::LazyRelations
30
17
 
31
- Her::API.new :url => "#{PuppetForge.host}/v3/" do |c|
32
- c.use PuppetForge::Middleware::JSONForHer
33
- c.use adapter
18
+ def initialize(json_response)
19
+ @attributes = json_response
20
+ orm_resp_item json_response
21
+ end
22
+
23
+ def orm_resp_item(json_response)
24
+ json_response.each do |key, value|
25
+ unless respond_to? key
26
+ define_singleton_method("#{key}") { @attributes[key] }
27
+ define_singleton_method("#{key}=") { |val| @attributes[key] = val }
28
+ end
34
29
  end
35
30
  end
36
31
 
32
+ # @return true if attribute exists, false otherwise
33
+ #
34
+ def has_attribute?(attr)
35
+ @attributes.has_key?(:"#{attr}")
36
+ end
37
+
38
+ def attribute(name)
39
+ @attributes[:"#{name}"]
40
+ end
41
+
42
+ def attributes
43
+ @attributes
44
+ end
45
+
37
46
  class << self
38
- # Overrides Her::Model#request to allow end users to dynamically update
39
- # both the Forge host being communicated with and the user agent string.
40
- #
41
- # @api private
42
- # @api her
43
- # @see Her::Model#request
44
- # @see PuppetForge.host
45
- # @see PuppetForge.user_agent
46
- def request(*args)
47
- unless her_api.base_uri =~ /^#{PuppetForge.host}/
48
- her_api.connection.url_prefix = "#{PuppetForge.host}/v3/"
47
+
48
+ include PuppetForge::Connection
49
+
50
+ API_VERSION = "v3"
51
+
52
+ def api_version
53
+ API_VERSION
54
+ end
55
+
56
+ # @private
57
+ def request(resource, item = nil, params = {})
58
+ unless conn.url_prefix =~ /^#{PuppetForge.host}/
59
+ conn.url_prefix = "#{PuppetForge.host}"
60
+ end
61
+
62
+
63
+ if item.nil?
64
+ uri_path = "/v3/#{resource}"
65
+ else
66
+ uri_path = "/v3/#{resource}/#{item}"
49
67
  end
50
68
 
51
- her_api.connection.headers[:user_agent] = %W[
52
- #{PuppetForge.user_agent}
53
- PuppetForge.gem/#{PuppetForge::VERSION}
54
- Her/#{Her::VERSION}
55
- Faraday/#{Faraday::VERSION}
56
- Ruby/#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL} (#{RUBY_PLATFORM})
57
- ].join(' ').strip
69
+ PuppetForge::V3::Base.conn.get uri_path, params
70
+ end
71
+
72
+ def find(slug)
73
+ return nil if slug.nil?
74
+
75
+ resp = request("#{self.name.split("::").last.downcase}s", slug)
58
76
 
59
- super
77
+ self.new(resp.body)
60
78
  end
61
79
 
62
- # Overrides Her::Model#new_collection with custom logic for handling the
63
- # paginated collections produced by the Forge API. These collections are
64
- # then wrapped in a {PaginatedCollection}, which enables navigation of
65
- # the paginated dataset.
66
- #
67
- # @api private
68
- # @api her
69
- # @param parsed_data [Hash<(:data, :errors)>] the parsed response data
70
- # @return [PaginatedCollection] the collection
71
- def new_collection(parsed_data)
72
- col = super :data => parsed_data[:data][:results] || [],
73
- :metadata => parsed_data[:data][:pagination] || { limit: 10, total: 0, offset: 0 },
74
- :errors => parsed_data[:errors]
75
-
76
- PaginatedCollection.new(self, col.to_a, col.metadata, col.errors)
80
+ def where(params)
81
+ resp = request("#{self.name.split("::").last.downcase}s", nil, params)
82
+
83
+ new_collection(resp)
77
84
  end
78
- end
79
85
 
80
- # FIXME: We should provide an actual unique identifier.
81
- primary_key :slug
82
- store_metadata :_metadata
83
- after_initialize do
84
- attributes[:slug] ||= uri[/[^\/]+$/]
85
- end
86
+ # Return a paginated collection of all modules
87
+ def all(params = {})
88
+ where(params)
89
+ end
86
90
 
87
- # Since our data is primarily URI based rather than ID based, we should
88
- # use our URIs as the request_path whenever possible.
89
- #
90
- # @api private
91
- # @api her
92
- # @see Her::Model::Paths#request_path
93
- def request_path(*args)
94
- if has_attribute? :uri then uri else super end
91
+ def get_collection(uri_path)
92
+ resource, params = split_uri_path uri_path
93
+ resp = request(resource, nil, params)
94
+
95
+ new_collection(resp)
96
+ end
97
+
98
+ # Faraday's Util#escape method will replace a '+' with '%2B' to prevent it being
99
+ # interpreted as a space. For compatibility with the Forge API, we would like a '+'
100
+ # to be interpreted as a space so they are changed to spaces here.
101
+ def convert_plus_to_space(str)
102
+ str.gsub(/[+]/, ' ')
103
+ end
104
+
105
+ # @private
106
+ def split_uri_path(uri_path)
107
+ all, resource, params = /(?:\/v3\/)([^\/]+)(?:\?)(.*)/.match(uri_path).to_a
108
+
109
+ params = convert_plus_to_space(params).split('&')
110
+
111
+ param_hash = Hash.new
112
+ params.each do |param|
113
+ key, val = param.split('=')
114
+ param_hash[key] = val
115
+ end
116
+
117
+ [resource, param_hash]
118
+ end
119
+
120
+ # @private
121
+ def new_collection(faraday_resp)
122
+ if faraday_resp[:errors].nil?
123
+ PaginatedCollection.new(self, faraday_resp.body[:results], faraday_resp.body[:pagination], nil)
124
+ else
125
+ PaginatedCollection.new(self)
126
+ end
127
+ end
95
128
  end
96
129
  end
97
130
  end