puppet_forge 1.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 (51) hide show
  1. data/.gitignore +22 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE.txt +13 -0
  4. data/README.md +172 -0
  5. data/Rakefile +2 -0
  6. data/lib/her/lazy_accessors.rb +140 -0
  7. data/lib/her/lazy_relations.rb +86 -0
  8. data/lib/puppet_forge.rb +19 -0
  9. data/lib/puppet_forge/middleware/json_for_her.rb +37 -0
  10. data/lib/puppet_forge/v3.rb +10 -0
  11. data/lib/puppet_forge/v3/base.rb +98 -0
  12. data/lib/puppet_forge/v3/base/paginated_collection.rb +73 -0
  13. data/lib/puppet_forge/v3/module.rb +15 -0
  14. data/lib/puppet_forge/v3/release.rb +35 -0
  15. data/lib/puppet_forge/v3/user.rb +21 -0
  16. data/lib/puppet_forge/version.rb +3 -0
  17. data/puppet_forge.gemspec +31 -0
  18. data/spec/fixtures/v3/files/puppetlabs-apache-0.0.1.tar.gz.headers +14 -0
  19. data/spec/fixtures/v3/files/puppetlabs-apache-0.0.1.tar.gz.json +0 -0
  20. data/spec/fixtures/v3/modules.headers +14 -0
  21. data/spec/fixtures/v3/modules.json +4197 -0
  22. data/spec/fixtures/v3/modules/puppetlabs-apache.headers +14 -0
  23. data/spec/fixtures/v3/modules/puppetlabs-apache.json +390 -0
  24. data/spec/fixtures/v3/modules?owner=puppetlabs.headers +14 -0
  25. data/spec/fixtures/v3/modules?owner=puppetlabs.json +4179 -0
  26. data/spec/fixtures/v3/modules?query=apache.headers +14 -0
  27. data/spec/fixtures/v3/modules?query=apache.json +3151 -0
  28. data/spec/fixtures/v3/releases.headers +14 -0
  29. data/spec/fixtures/v3/releases.json +3072 -0
  30. data/spec/fixtures/v3/releases/puppetlabs-apache-0.0.1.headers +14 -0
  31. data/spec/fixtures/v3/releases/puppetlabs-apache-0.0.1.json +93 -0
  32. data/spec/fixtures/v3/releases/puppetlabs-apache-0.0.2.headers +14 -0
  33. data/spec/fixtures/v3/releases/puppetlabs-apache-0.0.2.json +93 -0
  34. data/spec/fixtures/v3/releases/puppetlabs-apache-0.0.3.headers +14 -0
  35. data/spec/fixtures/v3/releases/puppetlabs-apache-0.0.3.json +93 -0
  36. data/spec/fixtures/v3/releases/puppetlabs-apache-0.0.4.headers +14 -0
  37. data/spec/fixtures/v3/releases/puppetlabs-apache-0.0.4.json +126 -0
  38. data/spec/fixtures/v3/releases/puppetlabs-apache-0.1.1.headers +14 -0
  39. data/spec/fixtures/v3/releases/puppetlabs-apache-0.1.1.json +140 -0
  40. data/spec/fixtures/v3/releases?module=puppetlabs-apache.headers +14 -0
  41. data/spec/fixtures/v3/releases?module=puppetlabs-apache.json +3287 -0
  42. data/spec/fixtures/v3/users/puppetlabs.headers +14 -0
  43. data/spec/fixtures/v3/users/puppetlabs.json +10 -0
  44. data/spec/spec_helper.rb +60 -0
  45. data/spec/unit/forge/v3/base/paginated_collection_spec.rb +88 -0
  46. data/spec/unit/forge/v3/module_spec.rb +118 -0
  47. data/spec/unit/forge/v3/release_spec.rb +112 -0
  48. data/spec/unit/forge/v3/user_spec.rb +50 -0
  49. data/spec/unit/her/lazy_accessors_spec.rb +142 -0
  50. data/spec/unit/her/lazy_relations_spec.rb +309 -0
  51. metadata +261 -0
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in puppet_forge.gemspec
4
+ gemspec
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2014 Puppet Labs, Inc.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
@@ -0,0 +1,172 @@
1
+ #Puppet Forge
2
+
3
+ Access and manipulate the [Puppet Forge API](https://forgeapi.puppetlabs.com)
4
+ from Ruby.
5
+
6
+ ##Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem 'puppet_forge'
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install puppet_forge
19
+
20
+ ##Requirements
21
+
22
+ * Ruby >= 1.9.3
23
+
24
+ ##Dependencies
25
+
26
+ * [Her](http://her-rb.org) ~> 0.6
27
+ * [Typhoeus](https://github.com/typhoeus/typhoeus) ~> 0.6 (optional)
28
+
29
+ Typhoeus will be used as the HTTP adapter if it is available, otherwise
30
+ Net::HTTP will be used. We recommend using Typhoeus for production-level
31
+ applications.
32
+
33
+ ##Usage
34
+
35
+ First, make sure you have imported the Puppet Forge gem into your application:
36
+
37
+ ``` ruby
38
+ require 'puppet_forge'
39
+ ```
40
+
41
+ Next, supply a user-agent string to identify requests sent by your application
42
+ to the Puppet Forge API:
43
+
44
+ ``` ruby
45
+ PuppetForge.user_agent = "MyApp/1.0.0"
46
+ ```
47
+
48
+ Now you can make use of the resource models defined by the gem:
49
+
50
+ * [PuppetForge::V3::User][user_ref]
51
+ * [PuppetForge::V3::Module][module_ref]
52
+ * [PuppetForge::V3::Release][release_ref]
53
+
54
+ For convenience, these classes are also aliased as:
55
+
56
+ * [PuppetForge::User][user_ref]
57
+ * [PuppetForge::Module][module_ref]
58
+ * [PuppetForge::Release][release_ref]
59
+
60
+ [user_ref]: https://github.com/puppetlabs/forge-ruby/wiki/Resource-Reference#puppetforgeuser
61
+ [module_ref]: https://github.com/puppetlabs/forge-ruby/wiki/Resource-Reference#puppetforgemodule
62
+ [release_ref]: https://github.com/puppetlabs/forge-ruby/wiki/Resource-Reference#puppetforgerelease
63
+
64
+ __The aliases will always point to the most modern API implementations for each
65
+ model.__ You may also use the fully qualified class names
66
+ (e.g. PuppetForge::V3::User) to ensure your code is forward compatible.
67
+
68
+ See the [Basic Interface](#basic-interface) section below for how to perform
69
+ common tasks with these models.
70
+
71
+ Please note that PuppetForge models are identified by unique slugs rather
72
+ than numerical identifiers.
73
+
74
+ The slug format, properties, associations, and methods available on each
75
+ resource model are documented on the [Resource Reference][resource_ref] page.
76
+
77
+ [resource_ref]: https://github.com/puppetlabs/forge-ruby/wiki/Resource-Reference
78
+
79
+ ###Basic Interface
80
+
81
+ Each of the models uses [Her](http://her-rb.org) (an ActiveRecord-like REST
82
+ library) to map over the Forge API endpoints. Most simple ActiveRecord-style
83
+ interactions function as intended.
84
+
85
+ Currently, only unauthenticated read-only actions are supported.
86
+
87
+ ``` ruby
88
+ # Find a Resource by Slug
89
+ PuppetForge::User.find('puppetlabs') # => #<Forge::V3::User(/v3/users/puppetlabs)>
90
+
91
+ # Find All Resources
92
+ PuppetForge::Module.all # See "Paginated Collections" below for important info about enumerating resource sets.
93
+
94
+ # Find Resources with Conditions
95
+ PuppetForge::Module.where(query: 'apache').all # See "Paginated Collections" below for important info about enumerating resource sets.
96
+ PuppetForge::Module.where(query: 'apache').first # => #<Forge::V3::Module(/v3/modules/puppetlabs-apache)>
97
+ ```
98
+
99
+ ###Paginated Collections
100
+
101
+ The Forge API only returns paginated collections as of v3.
102
+
103
+ ``` ruby
104
+ PuppetForge::Module.all.total # => 1728
105
+ PuppetForge::Module.all.length # => 20
106
+ ```
107
+
108
+ Movement through the collection can be simulated using the `limit` and `offset`
109
+ parameters, but it's generally preferable to leverage the pagination links
110
+ provided by the API. For convenience, pagination links are exposed by the
111
+ library.
112
+
113
+ ``` ruby
114
+ PuppetForge::Module.all.offset # => 0
115
+ PuppetForge::Module.all.next_url # => "/v3/modules?limit=20&offset=20"
116
+ PuppetForge::Module.all.next.offset # => 20
117
+ ```
118
+
119
+ An enumerator exists for iterating over the entire (unpaginated) collection.
120
+ Keep in mind that this will result in multiple calls to the Forge API.
121
+
122
+ ``` ruby
123
+ PuppetForge::Module.where(query: 'php').total # => 37
124
+ PuppetForge::Module.where(query: 'php').unpaginated # => #<Enumerator>
125
+ PuppetForge::Module.where(query: 'php').unpaginated.to_a.length # => 37
126
+ ```
127
+
128
+ ###Associations & Lazy Attributes
129
+
130
+ Associated models are accessible in the API by property name.
131
+
132
+ ``` ruby
133
+ PuppetForge::Module.find('puppetlabs-apache').owner # => #<Forge::V3::User(/v3/users/puppetlabs)>
134
+ ```
135
+
136
+ Properties of associated models are then loaded lazily.
137
+
138
+ ``` ruby
139
+ user = PuppetForge::Module.find('puppetlabs-apache').owner
140
+
141
+ # This does not trigger a request
142
+ user.username # => "puppetlabs"
143
+
144
+ # This *does* trigger a request
145
+ user.created_at # => "2010-05-19 05:46:26 -0700"
146
+ ```
147
+
148
+ ##Caveats
149
+
150
+ This library currently does no response caching of its own, instead opting to
151
+ re-issue every request each time. This will be changed in a later release.
152
+
153
+ ##Reporting Issues
154
+
155
+ Please report problems, issues, and feature requests on the public
156
+ [Puppet Labs issue tracker][issues] under the FORGE project. You will need
157
+ to create a free account to add new tickets.
158
+
159
+ [issues]: https://tickets.puppetlabs.com/browse/FORGE
160
+
161
+ ##Contributing
162
+
163
+ 1. Fork it ( https://github.com/[my-github-username]/forge-ruby/fork )
164
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
165
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
166
+ 4. Push to the branch (`git push origin my-new-feature`)
167
+ 5. Create a new Pull Request
168
+
169
+ ##Contributors
170
+
171
+ * Pieter van de Bruggen, Puppet Labs
172
+ * Jesse Scott, Puppet Labs
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,140 @@
1
+ # Her is an ORM for RESTful APIs, instead of databases.
2
+ # @see http://her-rb.org/
3
+ module Her
4
+
5
+ # ActiveRecord-like interface for RESTful models.
6
+ # @see http://her-rb.org/#usage/activerecord-like-methods
7
+ module Model; end
8
+
9
+ # When dealing with a remote service, it's reasonably common to receive only
10
+ # a partial representation of the underlying object, with additional data
11
+ # available upon request. {Her}, by default, provides a convenient interface
12
+ # for accessing whatever local data is available, but lacks good support for
13
+ # fleshing out partial representations. In order to build a seamless
14
+ # interface for both local and remote attriibutes, this module replaces the
15
+ # default behavior of {Her::Model}s with an "updatable" interface.
16
+ module LazyAccessors
17
+
18
+ # Callback for module inclusion.
19
+ #
20
+ # On each lazy class we'll store a reference to a Module, which will act as
21
+ # the container for the attribute methods.
22
+ #
23
+ # @param base [Class] the Class this module was included into
24
+ # @return [void]
25
+ def self.included(base)
26
+ base.singleton_class.class_eval do
27
+ attr_accessor :_accessor_container
28
+ end
29
+ end
30
+
31
+ # Override the default {Her::Model}#inspect behavior.
32
+ #
33
+ # The original behavior actually invokes each attribute accessor, which can
34
+ # be somewhat problematic when the accessors have been overridden. This
35
+ # implementation simply reports the contents of the attributes hash.
36
+ def inspect
37
+ attrs = attributes.map do |x, y|
38
+ [ x, attribute_for_inspect(y) ].join('=')
39
+ end
40
+ "#<#{self.class}(#{request_path}) #{attrs.join(' ')}>"
41
+ end
42
+
43
+ # Override the default {Her::Model}#method_misssing behavior.
44
+ #
45
+ # When we receive a {#method_missing} call, one of three things is true:
46
+ # - the caller is looking up a piece of local data without an accessor
47
+ # - the caller is looking up a piece of remote data
48
+ # - the method doesn't actually exist
49
+ #
50
+ # To solve the remote case, we begin by ensuring our attribute list is
51
+ # up-to-date with a call to {#fetch}, create a new {AccessorContainer} if
52
+ # necessary, and add any missing accessors to the container. We can then
53
+ # dispatch the method call to the newly created accessor.
54
+ #
55
+ # The local case is almost identical, except that we can skip updating the
56
+ # model's attributes.
57
+ #
58
+ # If, after our work, we haven't seen the requested method name, we can
59
+ # surmise that it doesn't actually exist, and pass the call along to
60
+ # upstream handlers.
61
+ def method_missing(name, *args, &blk)
62
+ fetch unless has_attribute?(name.to_s[/\w+/])
63
+
64
+ klass = self.class
65
+ mod = (klass._accessor_container ||= AccessorContainer.new(klass))
66
+ mod.add_attributes(attributes.keys)
67
+
68
+ if (meth = mod.instance_method(name) rescue nil)
69
+ return meth.bind(self).call(*args)
70
+ else
71
+ return super(name, *args, &blk)
72
+ end
73
+ end
74
+
75
+ # Updates model data from the API. This method will short-circuit if this
76
+ # model has already been fetched from the remote server, to avoid duplicate
77
+ # requests.
78
+ #
79
+ # @return [self]
80
+ def fetch
81
+ return self if @_fetch
82
+
83
+ klass = self.class
84
+ params = { :_method => klass.method_for(:find), :_path => self.request_path }
85
+
86
+ klass.request(params) do |data, response|
87
+ if @_fetch = response.success?
88
+ parsed = klass.parse(data[:data])
89
+ parsed.merge!(:_metadata => data[:metadata], :_errors => data[:errors])
90
+
91
+ self.send(:initialize, parsed)
92
+ self.run_callbacks(:find)
93
+ end
94
+ end
95
+
96
+ return self
97
+ end
98
+
99
+
100
+ # A Module subclass for attribute accessors.
101
+ class AccessorContainer < Module
102
+
103
+ # Creating a new instance of this class will automatically include itself
104
+ # into the provided class.
105
+ #
106
+ # @param base [Class] the class this container belongs to
107
+ def initialize(base)
108
+ base.send(:include, self)
109
+ end
110
+
111
+ # Adds attribute accessors, predicates, and mutators for the named keys.
112
+ # Since these methods will also be instantly available on all instances
113
+ # of the parent class, each of these methods will also conservatively
114
+ # {LazyAccessors#fetch} any missing data.
115
+ #
116
+ # @param keys [Array<#to_s>] the list of attributes to create
117
+ # @return [void]
118
+ def add_attributes(keys)
119
+ keys.each do |key|
120
+ next if methods.include?(name = :"#{key}")
121
+
122
+ define_method(name) do
123
+ fetch unless has_attribute?(name)
124
+ attribute(name)
125
+ end
126
+
127
+ define_method(:"#{name}?") do
128
+ fetch unless has_attribute?(name)
129
+ has_attribute?(name)
130
+ end
131
+
132
+ define_method(:"#{name}=") do |value|
133
+ fetch unless has_attribute?(name)
134
+ attributes[name] = value
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,86 @@
1
+ module Her
2
+
3
+ # This module provides convenience accessors for related resources. Related
4
+ # classes will include {LazyAccessors}, allowing them to transparently fetch
5
+ # fetch more complete representations from the API.
6
+ #
7
+ # @see LazyAccessors
8
+ module LazyRelations
9
+
10
+ # Mask mistaken `include` calls by transparently extending this class.
11
+ # @private
12
+ def self.included(base)
13
+ base.extend(self)
14
+ base.after_initialize { @_lazy = {} }
15
+ end
16
+
17
+ # @!macro [attach] lazy
18
+ # @!method $1
19
+ # Returns a lazily-loaded $1 proxy. To eagerly load this $1, call
20
+ # #fetch.
21
+ # @return a proxy for the related $1
22
+ #
23
+ # Declares a new lazily loaded property.
24
+ #
25
+ # This is particularly useful since our data hashes tend to contain at
26
+ # least a partial representation of the related object. This mechanism
27
+ # provides a proxy object which will avoid making HTTP requests until
28
+ # it is asked for a property it does not contain, at which point it
29
+ # will fetch the related object from the API and look up the property
30
+ # from there.
31
+ #
32
+ # @param name [Symbol] the name of the lazy attribute
33
+ # @param class_name [#to_s] the lazy relation's class name
34
+ def lazy(name, class_name = name)
35
+ parent = self.parent
36
+ klass = (class_name.is_a?(Class) ? class_name : nil)
37
+ class_name = "#{class_name}".singularize.classify
38
+
39
+ define_method(name) do
40
+ @_lazy[name] ||= begin
41
+ klass ||= parent.const_get(class_name)
42
+ klass.send(:include, Her::LazyAccessors)
43
+ fetch unless has_attribute?(name)
44
+ value = attribute(name)
45
+ klass.new(value) if value
46
+ end
47
+ end
48
+ end
49
+
50
+ # @!macro [attach] lazy_collection
51
+ # @!method $1
52
+ # Returns a lazily-loaded proxy for a collection of $1. To eagerly
53
+ # load any one of these $1, call #fetch.
54
+ # @return [Array<$2>] a proxy for the related collection of $1
55
+ #
56
+ # Declares a new lazily loaded collection.
57
+ #
58
+ # This behaves like {#lazy}, with the exception that the underlying
59
+ # attribute is an array of property hashes, representing several
60
+ # distinct models. In this case, we return an array of proxy objects,
61
+ # one for each property hash.
62
+ #
63
+ # It's also worth pointing out that this is not a paginated collection.
64
+ # Since the array of property hashes we're wrapping is itself
65
+ # unpaginated and complete (if shallow), wrapping it in a paginated
66
+ # collection doesn't provide any semantic value.
67
+ #
68
+ # @see LazyRelations
69
+ # @param name [Symbol] the name of the lazy collection attribute
70
+ # @param class_name [#to_s] the lazy relation's class name
71
+ def lazy_collection(name, class_name = name)
72
+ parent = self.parent
73
+ klass = (class_name.is_a?(Class) ? class_name : nil)
74
+ class_name = "#{class_name}".singularize.classify
75
+
76
+ define_method(name) do
77
+ @_lazy[name] ||= begin
78
+ klass ||= parent.const_get(class_name)
79
+ klass.send(:include, Her::LazyAccessors)
80
+ fetch unless has_attribute?(name)
81
+ (attribute(name) || []).map { |x| klass.new(x) }
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,19 @@
1
+ require 'bundler/setup'
2
+ require 'backports/1.9.1/enumerator/new' if RUBY_VERSION == '1.8.7'
3
+
4
+ require 'puppet_forge/version'
5
+
6
+ module PuppetForge
7
+ class << self
8
+ attr_accessor :user_agent
9
+ attr_accessor :host
10
+ end
11
+
12
+ self.host = 'https://forgeapi.puppetlabs.com'
13
+
14
+ require 'puppet_forge/v3'
15
+
16
+ const_set :User, PuppetForge::V3::User
17
+ const_set :Module, PuppetForge::V3::Module
18
+ const_set :Release, PuppetForge::V3::Release
19
+ end