puppet_forge 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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