rappfirst 0.1.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.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ config.*
2
+ spec/fixtures/**
3
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Andrew Gross
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.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Rappfirst
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'rappfirst'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install rappfirst
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
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
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.test_files = FileList['spec/lib/rappfirst/*_spec.rb']
5
+ t.verbose = true
6
+ end
7
+
8
+ task :default => :test
@@ -0,0 +1,44 @@
1
+ module Rappfirst
2
+ class Client
3
+ include HTTParty
4
+ format :json
5
+
6
+ def initialize(username=nil, api_key=nil)
7
+ if username.nil? || api_key.nil?
8
+ config = YAML::load( File.open( 'config.yml') )
9
+ username = config['username']
10
+ api_key = config['password']
11
+ end
12
+ self.class.basic_auth username, api_key
13
+ self.class.base_uri 'https://wwws.appfirst.com/api/v3'
14
+ end
15
+
16
+ def servers(query_string=nil)
17
+ response = get_servers(query_string)
18
+ if response.length == 1
19
+ return server(response.first['id'])
20
+ else
21
+ s = Array.new
22
+ response.each do |r|
23
+ s << server(r['id'])
24
+ end
25
+ return s
26
+ end
27
+ end
28
+
29
+ def server(id)
30
+ api_options = self.class.default_options
31
+ Rappfirst::Server.new(id, api_options=api_options)
32
+ end
33
+
34
+ private
35
+
36
+ def get_servers(query_string=nil)
37
+ if query_string && ! query_string.start_with?('?')
38
+ query_string = '?' + query_string
39
+ end
40
+ self.class.get("/servers/#{query_string}")
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,112 @@
1
+ module Rappfirst
2
+ class Server
3
+ include HTTParty
4
+ format :json
5
+
6
+ attr_accessor :id
7
+
8
+ def writeable?(name)
9
+ return ['description', 'nickname'].include?(name)
10
+ end
11
+
12
+ def initialize(id, api_options=nil)
13
+ if api_options && api_options.keys.include?(:basic_auth)
14
+ username = api_options[:basic_auth][:username]
15
+ api_key = api_options[:basic_auth][:api_key]
16
+ else
17
+ username = get_config('username')
18
+ api_key = get_config('password')
19
+ end
20
+
21
+ if api_options && api_options.keys.include?(:basic_uri)
22
+ base_uri = api_options[:base_uri]
23
+ else
24
+ base_uri = 'https://wwws.appfirst.com/api/v3'
25
+ end
26
+
27
+ self.class.basic_auth username, api_key
28
+ self.class.base_uri base_uri
29
+
30
+ self.id = id
31
+ set_attributes
32
+ end
33
+
34
+ def polled_data(refresh=false)
35
+ refresh ? @polled_data = get_polled_data : @polled_data ||= get_polled_data
36
+ end
37
+
38
+ def polled_data=(new_polled_data_config)
39
+ #set_polled_data(new_polled_data_config)
40
+ end
41
+
42
+ def outages(refresh=false)
43
+ refresh ? @outage_data = get_outage_data : @outage_data ||= get_outage_data
44
+ end
45
+
46
+ def tags(refresh=false)
47
+ refresh ? @tags = get_tags : @tags ||= get_tags
48
+ end
49
+
50
+ def delete
51
+ delete_self
52
+ end
53
+
54
+ private
55
+
56
+ def delete_self
57
+ response = self.class.delete("/servers/#{self.id}/")
58
+ unless response.code == 200
59
+ raise "Unable to delete server, received HTTP code #{response.code}"
60
+ end
61
+ end
62
+
63
+ def get_tags
64
+ return self.class.get("/servers/#{self.id}/tags/")['server_tags']
65
+ end
66
+
67
+ def get_outage_data
68
+ return self.class.get("/servers/#{self.id}/outages/")
69
+ end
70
+
71
+ def get_polled_data
72
+ return self.class.get("/servers/#{self.id}/polled_data_config/")
73
+ end
74
+
75
+ def set_polled_data(config)
76
+ return self.class.put("/servers/#{self.id}/polled_data_config/", config)
77
+ end
78
+
79
+ def set_attributes
80
+ response = get_attributes
81
+ response.each do |name, v|
82
+ create_method( "#{name}=".to_sym ) { |val|
83
+ if ! instance_variable_get("@" + name)
84
+ instance_variable_set("@" + name, val)
85
+ elsif self.writeable?(name) && instance_variable_get("@" + name)
86
+ instance_variable_set("@" + name, val)
87
+ end
88
+ }
89
+
90
+ create_method( name.to_sym ) {
91
+ instance_variable_get("@" + name)
92
+ }
93
+
94
+ instance_variable_set("@" + name, v)
95
+ end
96
+ end
97
+
98
+ def get_attributes
99
+ return self.class.get("/servers/#{self.id}/")
100
+ end
101
+
102
+ def create_method(name, &block)
103
+ self.class.send(:define_method, name, &block)
104
+ end
105
+
106
+ def get_config(key)
107
+ config = YAML::load( File.open('config.yml'))
108
+ return config[key]
109
+ end
110
+
111
+ end
112
+ end
@@ -0,0 +1,3 @@
1
+ module Rappfirst
2
+ VERSION = "0.1.0"
3
+ end
data/lib/rappfirst.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "httparty"
2
+
3
+ Dir[File.dirname(__FILE__) + '/rappfirst/*.rb'].each do |file|
4
+ require file
5
+ end
data/rappfirst.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rappfirst/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "rappfirst"
8
+ gem.version = Rappfirst::VERSION
9
+ gem.authors = ["Andrew Gross"]
10
+ gem.email = ["andrew.w.gross@gmail.com"]
11
+ gem.description = "See Readme.md"
12
+ gem.summary = "Appfirst API v3 Wrapper"
13
+ gem.homepage = "https://github.com/andrewgross/rappfirst"
14
+ gem.files = `git ls-files`.split($/)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ gem.require_paths = ["lib"]
18
+
19
+ gem.add_runtime_dependency 'httparty'
20
+
21
+ gem.add_development_dependency 'debugger'
22
+ gem.add_development_dependency 'webmock'
23
+ gem.add_development_dependency 'vcr'
24
+ gem.add_development_dependency 'turn'
25
+ gem.add_development_dependency 'rake'
26
+ end
@@ -0,0 +1,99 @@
1
+ require_relative '../../spec_helper'
2
+
3
+ describe Rappfirst::Client do
4
+
5
+ describe "default attributes" do
6
+
7
+ it "must include httparty methods" do
8
+ Rappfirst::Client.must_include HTTParty
9
+ end
10
+
11
+ it "must have the base url set to the Appfirst API" do
12
+ Rappfirst::Client.
13
+ instance_variable_get("@default_options")[:base_uri].
14
+ must_equal 'https://wwws.appfirst.com/api/v3'
15
+ end
16
+
17
+ it "must have API Credentials" do
18
+ Rappfirst::Client.
19
+ instance_variable_get("@default_options")[:basic_auth].
20
+ keys.must_include(:username)
21
+ end
22
+
23
+ end
24
+
25
+ describe "GET servers" do
26
+
27
+ let(:client) { Rappfirst::Client.new }
28
+
29
+ describe "Get all servers" do
30
+
31
+ before do
32
+ VCR.insert_cassette 'servers', :record => :new_episodes
33
+ end
34
+
35
+ after do
36
+ VCR.eject_cassette
37
+ end
38
+
39
+ it "must have a servers method" do
40
+ client.must_respond_to :servers
41
+ end
42
+
43
+ it "must parse the api response from JSON to Hash" do
44
+ client.servers.must_be_instance_of Array
45
+ end
46
+
47
+ it "should return an array of server objects" do
48
+ client.servers.each { |s| s.must_be_instance_of Rappfirst::Server}
49
+ end
50
+
51
+ end
52
+
53
+ describe "Get specific server" do
54
+
55
+ before do
56
+ VCR.insert_cassette 'search_for_server', :record => :new_episodes
57
+ end
58
+
59
+ after do
60
+ VCR.eject_cassette
61
+ end
62
+
63
+ it "must accept query parameters without a question mark" do
64
+ client.servers(query='hostname=yipit-linkedin-worker2').
65
+ must_be_instance_of Rappfirst::Server
66
+ end
67
+
68
+ it "should return a server object" do
69
+ client.servers(query='?hostname=yipit-linkedin-worker2').
70
+ must_be_instance_of Rappfirst::Server
71
+ end
72
+
73
+ end
74
+
75
+ end
76
+
77
+ describe "Query single server" do
78
+
79
+ let(:client) { Rappfirst::Client.new }
80
+
81
+ before do
82
+ VCR.insert_cassette 'single_server', :record => :new_episodes
83
+ end
84
+
85
+ after do
86
+ VCR.eject_cassette
87
+ end
88
+
89
+ it "must have a server method" do
90
+ client.must_respond_to :server
91
+ end
92
+
93
+ it "must return a server object" do
94
+ client.server('11743').must_be_instance_of Rappfirst::Server
95
+ end
96
+
97
+ end
98
+
99
+ end
@@ -0,0 +1,248 @@
1
+ require_relative '../../spec_helper'
2
+
3
+ describe Rappfirst::Server do
4
+
5
+ describe "default attributes" do
6
+
7
+ it "must include httparty methods" do
8
+ Rappfirst::Server.must_include HTTParty
9
+ end
10
+
11
+ it "must have the base url set to the Appfirst API" do
12
+ Rappfirst::Server.
13
+ instance_variable_get("@default_options")[:base_uri].
14
+ must_equal 'https://wwws.appfirst.com/api/v3'
15
+ end
16
+
17
+ it "must have API Credentials" do
18
+ Rappfirst::Client.
19
+ instance_variable_get("@default_options")[:basic_auth].
20
+ keys.must_include(:username)
21
+ end
22
+
23
+ end
24
+
25
+
26
+
27
+ describe "attributes" do
28
+
29
+ let(:server) { Rappfirst::Server.new('11743') }
30
+
31
+ before do
32
+ VCR.insert_cassette 'server', :record => :new_episodes
33
+ end
34
+
35
+ after do
36
+ VCR.eject_cassette
37
+ end
38
+
39
+ describe "retrieve and create methods" do
40
+
41
+ it "must populate the hostname attribute" do
42
+ server.must_respond_to :hostname
43
+ end
44
+
45
+ it "must make the hostname attribute read-only" do
46
+ h = server.hostname
47
+ server.hostname = "foobar"
48
+ server.hostname.must_equal h
49
+ end
50
+
51
+ it "must populate the nickname attribute" do
52
+ server.must_respond_to :nickname
53
+ end
54
+
55
+ it "must make the nickname attribute writeable" do
56
+ n = server.nickname
57
+ server.nickname = "foobar"
58
+ server.nickname.wont_equal n
59
+ end
60
+
61
+ end
62
+
63
+ describe "cache data" do
64
+
65
+ before do
66
+ server.polled_data
67
+ stub_request(:any, /wwws.appfirst.com/).to_timeout
68
+ end
69
+
70
+ it "must cache attributes" do
71
+ skip "TODO: Implement Refreshing of base attributes"
72
+ lambda { server.hostname }.must_raise Timeout::Error
73
+ end
74
+
75
+ end
76
+
77
+ end
78
+
79
+
80
+
81
+ describe "polled data config" do
82
+
83
+ let(:server) { Rappfirst::Server.new('11743') }
84
+
85
+ before do
86
+ VCR.insert_cassette 'server', :record => :new_episodes
87
+ end
88
+
89
+ after do
90
+ VCR.eject_cassette
91
+ end
92
+
93
+ it "must have a polled data method" do
94
+ server.must_respond_to :polled_data
95
+ end
96
+
97
+ it "must fetch the current polled data config" do
98
+ server.polled_data.must_be_instance_of Hash
99
+ end
100
+
101
+ it "must have proper results" do
102
+ ["file_type", "file_contents", "server_id", "file_path", "frequency"].each do |key|
103
+ server.polled_data.keys.must_include(key)
104
+ end
105
+ end
106
+
107
+ it "must have a polled data setter" do
108
+ server.must_respond_to :polled_data=
109
+ end
110
+
111
+ it "must upload new configs" do
112
+ skip "API is returning 500 Errors"
113
+ current_data = server.polled_data.clone
114
+ new_data = current_data.clone
115
+ new_data['frequency'] = 600
116
+ server.polled_data = new_data
117
+ server.polled_data(refresh=true)['frequency'].wont_equal current_data['frequency']
118
+ end
119
+
120
+ before do
121
+ server.polled_data
122
+ stub_request(:any, /wwws.appfirst.com/).to_timeout
123
+ end
124
+
125
+ it "must refresh the results if forced" do
126
+ lambda { server.polled_data(refresh=true) }.must_raise Timeout::Error
127
+ end
128
+
129
+ end
130
+
131
+
132
+
133
+ describe "outages" do
134
+
135
+ let(:server) { Rappfirst::Server.new('11743') }
136
+
137
+ before do
138
+ VCR.insert_cassette 'server', :record => :new_episodes
139
+ end
140
+
141
+ after do
142
+ VCR.eject_cassette
143
+ end
144
+
145
+ describe "retrieve outages" do
146
+
147
+ it "must have an outages method" do
148
+ server.must_respond_to :outages
149
+ end
150
+
151
+ it "must retrieve outage data" do
152
+ server.outages.must_be_instance_of Array
153
+ end
154
+
155
+ end
156
+
157
+ describe "cached data" do
158
+
159
+ before do
160
+ server.outages
161
+ stub_request(:any, /wwws.appfirst.com/).to_timeout
162
+ end
163
+
164
+ it "must cache outage data" do
165
+ lambda { server.outages(refresh = true) }.must_raise Timeout::Error
166
+ end
167
+
168
+ end
169
+
170
+ end
171
+
172
+
173
+
174
+ describe "tags" do
175
+
176
+ let(:server) { Rappfirst::Server.new('11743') }
177
+
178
+ before do
179
+ VCR.insert_cassette 'server', :record => :new_episodes
180
+ end
181
+
182
+ after do
183
+ VCR.eject_cassette
184
+ end
185
+
186
+ describe "retrieve tags" do
187
+
188
+ it "must have a tags method" do
189
+ server.must_respond_to :tags
190
+ end
191
+
192
+ it "must retrieve tag data" do
193
+ server.tags.must_be_instance_of Array
194
+ end
195
+
196
+ end
197
+
198
+ describe "cached data" do
199
+
200
+ before do
201
+ server.tags
202
+ stub_request(:any, /wwws.appfirst.com/).to_timeout
203
+ end
204
+
205
+ it "must cache outage data" do
206
+ lambda { server.tags(refresh = true) }.must_raise Timeout::Error
207
+ end
208
+
209
+ end
210
+
211
+ end
212
+
213
+ describe "delete server" do
214
+
215
+ let(:server) { Rappfirst::Server.new('245342') }
216
+
217
+ before do
218
+ VCR.insert_cassette 'server', :record => :new_episodes
219
+ end
220
+
221
+ after do
222
+ VCR.eject_cassette
223
+ end
224
+
225
+ describe "signature" do
226
+
227
+ it "must have a deletion method" do
228
+ server.must_respond_to :delete
229
+ end
230
+
231
+ end
232
+
233
+ describe "failed deletion" do
234
+
235
+ before do
236
+ stub_request(:any, /wwws.appfirst.com/).
237
+ to_return(:status => [500, "Internal Server Error"])
238
+ end
239
+
240
+ it "must delete itself" do
241
+ lambda { server.delete }.must_raise RuntimeError
242
+ end
243
+
244
+ end
245
+
246
+ end
247
+
248
+ end
@@ -0,0 +1,22 @@
1
+ require_relative "../lib/rappfirst"
2
+
3
+ require 'minitest/autorun'
4
+ require "minitest/spec"
5
+ require 'webmock/minitest'
6
+ require 'vcr'
7
+ require 'turn'
8
+
9
+ # Turn Config
10
+ Turn.config do |c|
11
+ # :outline - turn's original case/test outline mode [default]
12
+ c.format = :outline
13
+ # use humanized test names (works only with :outline format)
14
+ c.natural = true
15
+ end
16
+
17
+ # VCR config
18
+ VCR.configure do |c|
19
+ c.cassette_library_dir = 'spec/fixtures/rappfirst_cassettes'
20
+ c.hook_into :webmock
21
+ end
22
+
metadata ADDED
@@ -0,0 +1,157 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rappfirst
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Andrew Gross
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-05-06 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: httparty
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
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'
30
+ - !ruby/object:Gem::Dependency
31
+ name: debugger
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
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: webmock
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: vcr
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
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: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: turn
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
+ - !ruby/object:Gem::Dependency
95
+ name: rake
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ description: See Readme.md
111
+ email:
112
+ - andrew.w.gross@gmail.com
113
+ executables: []
114
+ extensions: []
115
+ extra_rdoc_files: []
116
+ files:
117
+ - .gitignore
118
+ - Gemfile
119
+ - LICENSE.txt
120
+ - README.md
121
+ - Rakefile
122
+ - lib/rappfirst.rb
123
+ - lib/rappfirst/client.rb
124
+ - lib/rappfirst/server.rb
125
+ - lib/rappfirst/version.rb
126
+ - rappfirst.gemspec
127
+ - spec/lib/rappfirst/client_spec.rb
128
+ - spec/lib/rappfirst/server_spec.rb
129
+ - spec/spec_helper.rb
130
+ homepage: https://github.com/andrewgross/rappfirst
131
+ licenses: []
132
+ post_install_message:
133
+ rdoc_options: []
134
+ require_paths:
135
+ - lib
136
+ required_ruby_version: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ none: false
144
+ requirements:
145
+ - - ! '>='
146
+ - !ruby/object:Gem::Version
147
+ version: '0'
148
+ requirements: []
149
+ rubyforge_project:
150
+ rubygems_version: 1.8.23
151
+ signing_key:
152
+ specification_version: 3
153
+ summary: Appfirst API v3 Wrapper
154
+ test_files:
155
+ - spec/lib/rappfirst/client_spec.rb
156
+ - spec/lib/rappfirst/server_spec.rb
157
+ - spec/spec_helper.rb