appfirst 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
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
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in appfirst.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem "awesome_print"
8
+ gem "pry-nav"
9
+ gem "rspec", "~> 2.14"
10
+ gem "guard-rspec"
11
+ end
@@ -0,0 +1,9 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :rspec, cmd: "bundle exec rspec" do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { "spec" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
9
+
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Josh Lane & Thom Mahoney
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ # Appfirst
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'appfirst'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install appfirst
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it ( http://github.com/<my-github-username>/appfirst/fork )
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'appfirst/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "appfirst"
8
+ spec.version = Appfirst::VERSION
9
+ spec.authors = ["Josh Lane & Thom Mahoney"]
10
+ spec.email = ["jlane@engineyard.com"]
11
+ spec.summary = %q{A ruby client for the AppFirst API}
12
+ spec.description = ""
13
+ spec.homepage = "http://github.com/engineyard/appfirst"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "faraday", "~> 0.8.9"
22
+ spec.add_dependency "faraday_middleware"
23
+ spec.add_dependency "cistern", "~> 0.2.3"
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.5"
26
+ spec.add_development_dependency "rake"
27
+ end
@@ -0,0 +1,14 @@
1
+ require "appfirst/version"
2
+
3
+ require 'logger'
4
+ require 'faraday'
5
+ require 'faraday_middleware'
6
+ require 'cistern'
7
+
8
+ module Appfirst
9
+ require 'appfirst/collection'
10
+ require 'appfirst/logger'
11
+ require 'appfirst/response'
12
+
13
+ require 'appfirst/client'
14
+ end
@@ -0,0 +1,132 @@
1
+ class Appfirst::Client < Cistern::Service
2
+ model_path "appfirst/models"
3
+ request_path "appfirst/requests"
4
+
5
+ #collection :servers
6
+ collection :server_tags
7
+
8
+ model :server
9
+ model :server_tag
10
+
11
+ request :create_server_tag
12
+ #request :get_server
13
+ #request :get_server_tags
14
+ #request :get_servers
15
+ #request :get_servers_tags
16
+ #request :get_tag_servers
17
+ #request :set_servers_tags
18
+
19
+ recognizes :url, :logger, :adapter, :connection_options, :user, :password, :path
20
+
21
+ module Shared
22
+ attr_reader :url, :user, :path
23
+
24
+ def setup(options)
25
+ @url = options[:url] || ENV["APPFIRST_URL"] || "https://engineyard.appfirst.com"
26
+ @path = options[:path] || "/api/v3"
27
+
28
+ # For auth
29
+ @user = options[:user]
30
+ @password = options[:password]
31
+
32
+ @logger = options[:logger] || Logger.new(nil)
33
+ end
34
+ end
35
+
36
+ class Real
37
+ include Shared
38
+
39
+ def initialize(options={})
40
+ setup(options)
41
+ adapter = options[:adapter] || Faraday.default_adapter
42
+ connection_options = options[:connection_options] || {ssl: {verify: false}}
43
+
44
+ @connection = Faraday.new({url: @url}.merge(connection_options)) do |builder|
45
+ builder.basic_auth @user, @password
46
+ builder.request :url_encoded
47
+ builder.response :json, content_type: /\bjson$/
48
+ builder.adapter adapter
49
+ builder.request :retry, max: 10, interval: 0.05, interval_randomness: 0.5, backoff_factor: 2
50
+
51
+ builder.use Appfirst::Logger, @logger
52
+
53
+ builder.adapter *adapter
54
+ end
55
+ end
56
+
57
+ def request(method, request_path, params = nil)
58
+ response = @connection.send(method) do |req|
59
+ req.url(File.join(@url, path, request_path, "/"))
60
+ req.params.merge!(params)
61
+ end
62
+
63
+ Appfirst::Response.new(response.status, response.headers, response.body).raise!
64
+ end
65
+ end # Real
66
+
67
+ class Mock
68
+ include Shared
69
+
70
+ def self.data
71
+ @data ||= Hash.new do |h,k|
72
+ {
73
+ :servers => {},
74
+ :server_tags => {},
75
+ }
76
+ end
77
+ end
78
+
79
+ def data
80
+ self.class.data[self.url]
81
+ end
82
+
83
+ def self.reset!
84
+ @data = nil
85
+ @serial_id = 0
86
+ end
87
+
88
+ def initialize(options={})
89
+ setup(options)
90
+ end
91
+
92
+ def response(options={})
93
+ status = options[:status] || 200
94
+ body = options[:body]
95
+ headers = {
96
+ "Content-Type" => "application/json; charset=utf-8"
97
+ }.merge(options[:headers] || {})
98
+
99
+ Appfirst::Response.new(status, headers, body).raise!
100
+ end
101
+
102
+ def url_for(path)
103
+ File.join(@url.to_s, path.to_s)
104
+ end
105
+
106
+ def stringify_keys(hash)
107
+ hash.is_a?(Hash) ? hash.inject({}){|r,(k,v)| r.merge(k.to_s => stringify_keys(v))} : hash
108
+ end
109
+
110
+ def resource_uri(fragment)
111
+ File.join(path, fragment)
112
+ end
113
+
114
+ def require_parameter(params, param, options={})
115
+ if value = params[param]
116
+ value
117
+ else
118
+ errors = [{"message" => (options[:message] || "Missing parameter: #{param}"), "code" => (options[:code] || -1)}]
119
+
120
+ response(
121
+ :body => {"errors" => errors},
122
+ :status => 400,
123
+ )
124
+ end
125
+ end
126
+
127
+ def serial_id
128
+ @@serial_id ||= 0
129
+ @@serial_id += 1
130
+ end
131
+ end # Mock
132
+ end # Appfirst::Client
@@ -0,0 +1,58 @@
1
+ module Appfirst::Collection
2
+ def self.included(klass)
3
+ klass.attribute :next_link
4
+ klass.attribute :prev_link
5
+ klass.attribute :last_link
6
+ klass.attribute :total_count
7
+ klass.attribute :url
8
+ klass.send(:extend, Appfirst::Collection::Attributes)
9
+ end
10
+
11
+ module Attributes
12
+ attr_accessor :model_root, :model_request, :collection_root, :collection_request
13
+ end
14
+
15
+ def create!(*args)
16
+ model = self.new(*args)
17
+ model.save!
18
+ end
19
+
20
+ def model_root
21
+ self.class.model_root
22
+ end
23
+
24
+ def model_request
25
+ self.class.model_request
26
+ end
27
+
28
+ def get(id)
29
+ if data = connection.send(self.model_request, "id" => id).body[self.model_root]
30
+ new(data)
31
+ else
32
+ nil
33
+ end
34
+ rescue Appfirst::Response::NotFound
35
+ nil
36
+ end
37
+
38
+ def collection_root
39
+ self.class.instance_variable_get(:@collection_root)
40
+ end
41
+
42
+ def collection_request
43
+ self.class.instance_variable_get(:@collection_request)
44
+ end
45
+
46
+ def ==(comparison_object)
47
+ comparison_object.equal?(self) ||
48
+ (comparison_object.is_a?(self.class) &&
49
+ comparison_object.map(&:identity) == self.map(&:identity))
50
+ end
51
+
52
+ def all(params={})
53
+ params["url"] ||= self.url
54
+
55
+ response = self.connection.send(self.collection_request, params)
56
+ load_page(response)
57
+ end
58
+ end
@@ -0,0 +1,31 @@
1
+ class Appfirst::Logger < Faraday::Response::Middleware
2
+ extend Forwardable
3
+
4
+ def initialize(app, logger = nil)
5
+ super(app)
6
+ @logger = logger || ::Logger.new(nil)
7
+ end
8
+
9
+ def_delegators :@logger, :debug, :info, :warn, :error, :fatal
10
+
11
+ def call(env)
12
+ info "REQUEST: #{env[:method].upcase} #{env[:url].to_s}"
13
+ info('request') { dump_headers env[:request_headers] }
14
+ info('request.body') { env[:body] } if (env[:request_headers]["Accept"] || "").match("application/json")
15
+ info('')
16
+ super
17
+ end
18
+
19
+ def on_complete(env)
20
+ info "RESPONSE: #{env[:status]}"
21
+ info('response') { dump_headers env[:response_headers] }
22
+ info('response.body') { env[:body] } if (env[:response_headers]["Content-Type"] || "").match("application/json")
23
+ info('')
24
+ end
25
+
26
+ private
27
+
28
+ def dump_headers(headers)
29
+ headers.map { |k, v| "#{k}: #{v.inspect}" }.join("\n")
30
+ end
31
+ end
@@ -0,0 +1,20 @@
1
+ class Appfirst::Client::Server < Cistern::Model
2
+ identity :id, type: :integer # (read-only) Unique server ID.
3
+
4
+ attribute :architecture, type: :string # (read-only) The architecture of the server.
5
+ attribute :capacity_cpu_freq, type: :integer # (read-only) The frequency of the CPU in MHz.
6
+ attribute :capacity_cpu_num, type: :integer # (read-only) The number of CPU cores on the system.
7
+ attribute :capacity_disks # (read-only) Mapping of the names of the disks (mount points) to their capacity in MB.
8
+ attribute :capacity_mem, type: :integer # (read-only) The total available memory on the server in bytes.
9
+ attribute :created, type: :integer # (read-only) The time the server was created in UTC seconds.
10
+ attribute :current_version, type: :string # (read-only) Version of AppFirst collector that’s running on the server.
11
+ attribute :description, type: :string # The user-defined description of the server.
12
+ attribute :distribution, type: :string # (read-only) The OS distribution e.g. Ubuntu, RedHat.
13
+ attribute :hostname, type: :string # (read-only) The hostname for this server.
14
+ attribute :kernel_version, type: :string # (read-only) The specific kernel version of the server’s OS.
15
+ attribute :location_ip, type: :string # (read-only) IP address that collector is uploading from.
16
+ attribute :nickname, type: :string # The user-defined nickname of the server.
17
+ attribute :os, type: :string # (read-only) The OS for this server, either Windows or Linux.
18
+ attribute :resource_uri, type: :string # (read-only) The URI to get more information about this item.
19
+ attribute :running, type: :boolean # (read-only) Boolean indicating whether the server is currently uploading data to AppFirst.
20
+ end
@@ -0,0 +1,19 @@
1
+ class Appfirst::Client::ServerTag < Cistern::Model
2
+ identity :id, type: :integer # (read-only) Unique server tag ID.
3
+
4
+ attribute :name, type: :string # The name of the server tag.
5
+ attribute :resource_uri, type: :string # (read-only) The URI to get more information about this item.
6
+ attribute :server_ids, type: :array, aliases: ['servers'] # IDs of servers that belong to this server tag.
7
+
8
+ def servers
9
+ self.server_ids.map { |server_id| self.connection.servers.get(server_id) }
10
+ end
11
+
12
+ def save
13
+ if new_record?
14
+ self.collection.new(self.connection.create_server_tag(Cistern::Hash.slice(self.attributes, :name, :servers)).body)
15
+ else
16
+ raise NotImplementedError # update tag
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,10 @@
1
+ class Appfirst::Client::ServerTags < Cistern::Collection
2
+ include Appfirst::Collection
3
+
4
+ model Appfirst::Client::ServerTag
5
+
6
+ self.model_root = "server_tags"
7
+ self.model_request = :get_server_tag
8
+ self.collection_root = "server_tags"
9
+ self.collection_request = :get_server_tags
10
+ end
@@ -0,0 +1,33 @@
1
+ class Appfirst::Client
2
+ class Real
3
+ def create_server_tag(params={})
4
+ request(:post, "/server_tags", params)
5
+ end
6
+ end # Real
7
+
8
+ class Mock
9
+ def create_server_tag(params={})
10
+ identity = self.serial_id
11
+
12
+ name = require_parameter(params, :name, code: 5, message: "Invalid server tag name, name must be provided") # {"errors"=>[{"message"=>"Invalid server tag name, name must be provided", "code"=>5}]}
13
+ servers = params[:servers] || []
14
+
15
+ response_hash = {
16
+ :id => identity,
17
+ :name => name,
18
+ :servers => servers,
19
+ :resource_uri => resource_uri("/server_tags/#{identity}")
20
+ }
21
+
22
+ self.data[:server_tags][identity] = response_hash
23
+
24
+ response(
25
+ :body => response_hash,
26
+ :status => 200,
27
+ :headers => {
28
+ "Content-Type" => "application/json; charset=utf8"
29
+ }
30
+ )
31
+ end
32
+ end # Mock
33
+ end # Appfirst::Client
@@ -0,0 +1,41 @@
1
+ class Appfirst::Response
2
+ class Error < StandardError
3
+ attr_reader :response
4
+
5
+ def initialize(response)
6
+ @response = response
7
+ super({status: response.status, headers: response.headers, body: response.body}.inspect)
8
+ end
9
+ end
10
+
11
+ BadRequest = Class.new(Error)
12
+ NotFound = Class.new(Error)
13
+ Unprocessable = Class.new(Error)
14
+ Conflict = Class.new(Error)
15
+ Unexpected = Class.new(Error)
16
+
17
+ EXCEPTION_MAPPING = {
18
+ 400 => BadRequest,
19
+ 404 => NotFound,
20
+ 409 => Conflict,
21
+ 422 => Unprocessable,
22
+ 500 => Unexpected,
23
+ }
24
+
25
+ attr_reader :headers, :status, :body
26
+
27
+ def initialize(status, headers, body)
28
+ @status, @headers, @body = status, headers, body
29
+ end
30
+
31
+ def successful?
32
+ self.status < 300 && self.status > 199 || self.status == 304
33
+ end
34
+
35
+ def raise!
36
+ if !successful?
37
+ raise (EXCEPTION_MAPPING[self.status] || Error).new(self)
38
+ else self
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,3 @@
1
+ module Appfirst
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe "server_tags" do
4
+ let!(:client) { create_client }
5
+
6
+ it "should create a server tag" do
7
+ name = "josh"
8
+
9
+ server_tag = client.server_tags.create(name: name)
10
+ server_tag.name.should == name
11
+ server_tag.resource_uri.should_not be_nil
12
+ server_tag.server_ids.should be_empty
13
+
14
+ server_tag.servers.should be_empty
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ ENV['MOCK_APPFIRST'] ||= 'true'
2
+ ENV['APPFIRST_URL'] ||= "http://appfirst.localdev.engineyard.com:3000"
3
+
4
+ Bundler.require(:test)
5
+
6
+ require File.expand_path("../../lib/appfirst", __FILE__)
7
+ Dir[File.expand_path("../{shared,support}/*.rb", __FILE__)].each{|f| require(f)}
8
+
9
+ Cistern.formatter = Cistern::Formatter::AwesomePrint
10
+
11
+ if ENV['MOCK_APPFIRST'] == 'true'
12
+ Cistern.timeout = 0
13
+ Appfirst::Client.mock!
14
+ else
15
+ Cistern.timeout = 10
16
+ end
17
+
18
+ RSpec.configure do |config|
19
+ config.order = :random
20
+
21
+ config.before(:each) do
22
+ Appfirst::Client.mocking? && Appfirst::Client.reset!
23
+ end
24
+ end
@@ -0,0 +1,15 @@
1
+ module ClientHelper
2
+ def create_client(attributes={})
3
+ user = ENV["APPFIRST_USER"] || attributes[:user] || "engineyard"
4
+ password = ENV["APPFIRST_PASSWORD"] || attributes[:password] || "dontworryaboutit"
5
+
6
+ merged_attributes = {user: user, password: password}
7
+ merged_attributes.merge!(logger: Logger.new(STDOUT)) if ENV['VERBOSE']
8
+
9
+ Appfirst::Client.new(merged_attributes)
10
+ end
11
+ end
12
+
13
+ RSpec.configure do |config|
14
+ config.include(ClientHelper)
15
+ end
metadata ADDED
@@ -0,0 +1,150 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: appfirst
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Josh Lane & Thom Mahoney
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-01-29 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: faraday
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.8.9
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 0.8.9
30
+ - !ruby/object:Gem::Dependency
31
+ name: faraday_middleware
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: cistern
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 0.2.3
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 0.2.3
62
+ - !ruby/object:Gem::Dependency
63
+ name: bundler
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: '1.5'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: '1.5'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rake
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ description: ''
95
+ email:
96
+ - jlane@engineyard.com
97
+ executables: []
98
+ extensions: []
99
+ extra_rdoc_files: []
100
+ files:
101
+ - .gitignore
102
+ - Gemfile
103
+ - Guardfile
104
+ - LICENSE.txt
105
+ - README.md
106
+ - Rakefile
107
+ - appfirst.gemspec
108
+ - lib/appfirst.rb
109
+ - lib/appfirst/client.rb
110
+ - lib/appfirst/collection.rb
111
+ - lib/appfirst/logger.rb
112
+ - lib/appfirst/models/server.rb
113
+ - lib/appfirst/models/server_tag.rb
114
+ - lib/appfirst/models/server_tags.rb
115
+ - lib/appfirst/requests/create_server_tag.rb
116
+ - lib/appfirst/response.rb
117
+ - lib/appfirst/version.rb
118
+ - spec/server_tags_spec.rb
119
+ - spec/spec_helper.rb
120
+ - spec/support/client_helper.rb
121
+ homepage: http://github.com/engineyard/appfirst
122
+ licenses:
123
+ - MIT
124
+ post_install_message:
125
+ rdoc_options: []
126
+ require_paths:
127
+ - lib
128
+ required_ruby_version: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ required_rubygems_version: !ruby/object:Gem::Requirement
135
+ none: false
136
+ requirements:
137
+ - - ! '>='
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ requirements: []
141
+ rubyforge_project:
142
+ rubygems_version: 1.8.23
143
+ signing_key:
144
+ specification_version: 3
145
+ summary: A ruby client for the AppFirst API
146
+ test_files:
147
+ - spec/server_tags_spec.rb
148
+ - spec/spec_helper.rb
149
+ - spec/support/client_helper.rb
150
+ has_rdoc: