strobe 0.0.1 → 0.0.2
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/bin/strobe +2 -12
- data/lib/strobe.rb +35 -88
- data/lib/strobe/association.rb +86 -0
- data/lib/strobe/cli.rb +39 -53
- data/lib/strobe/cli/action.rb +131 -0
- data/lib/strobe/cli/actions.rb +103 -0
- data/lib/strobe/cli/settings.rb +62 -0
- data/lib/strobe/collection.rb +53 -0
- data/lib/strobe/connection.rb +91 -0
- data/lib/strobe/resource/base.rb +235 -0
- data/lib/strobe/resource/collection.rb +50 -0
- data/lib/strobe/resource/singleton.rb +18 -0
- data/lib/strobe/resources/account.rb +12 -0
- data/lib/strobe/resources/application.rb +129 -0
- data/lib/strobe/resources/me.rb +11 -0
- data/lib/strobe/resources/signup.rb +27 -0
- data/lib/strobe/resources/team.rb +13 -0
- data/lib/strobe/resources/user.rb +11 -0
- data/lib/strobe/validations.rb +118 -0
- metadata +50 -124
- data/.gitignore +0 -5
- data/Gemfile +0 -4
- data/Gemfile.lock +0 -69
- data/Rakefile +0 -36
- data/features/1_sign_up.feature +0 -16
- data/features/2_deploying.feature +0 -35
- data/features/3_deploying_sproutcore_apps.feature +0 -19
- data/features/step_definitions/cli_steps.rb +0 -8
- data/features/step_definitions/deploying_steps.rb +0 -58
- data/features/step_definitions/sign_up.rb +0 -74
- data/features/support/helpers.rb +0 -104
- data/features/support/strobe.rb +0 -57
- data/lib/strobe/account.rb +0 -67
- data/lib/strobe/deploy.rb +0 -131
- data/lib/strobe/errors.rb +0 -12
- data/lib/strobe/http.rb +0 -94
- data/lib/strobe/server.rb +0 -13
- data/lib/strobe/settings.rb +0 -56
- data/lib/strobe/ui.rb +0 -81
- data/lib/strobe/user.rb +0 -9
- data/lib/strobe/version.rb +0 -3
- data/strobe.gemspec +0 -35
@@ -0,0 +1,103 @@
|
|
1
|
+
module Strobe
|
2
|
+
module CLI::Actions
|
3
|
+
class SignupAction < CLI::Action
|
4
|
+
def steps
|
5
|
+
resource Signup.new :account => { :name => 'default' }
|
6
|
+
|
7
|
+
if settings[:token]
|
8
|
+
say "This computer is already registered with a Strobe account."
|
9
|
+
unless agree "Signup anyway? [Yn] "
|
10
|
+
say "exiting..."
|
11
|
+
exit
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
say "Please fill out the following information."
|
16
|
+
|
17
|
+
ask :invitation
|
18
|
+
ask :email, :into => "user.email"
|
19
|
+
|
20
|
+
group :validate => "user.password" do
|
21
|
+
password :password, :into => "user.password"
|
22
|
+
password :password_confirmation, :into => "password_confirmation"
|
23
|
+
end
|
24
|
+
|
25
|
+
save :on_error => :restart do
|
26
|
+
settings[:token] = resource['user.authentication_token']
|
27
|
+
settings.authenticate!
|
28
|
+
say "Your account was created successfully."
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class LoginAction < CLI::Action
|
34
|
+
def steps
|
35
|
+
resource Me.new
|
36
|
+
|
37
|
+
say "Please enter your Strobe username and password."
|
38
|
+
|
39
|
+
ask :email, :into => "user.email"
|
40
|
+
password :password, :into => "user.password"
|
41
|
+
|
42
|
+
save :on_error => :restart do
|
43
|
+
settings[:token] = resource['user.authentication_token']
|
44
|
+
settings.authenticate!
|
45
|
+
say "The computer has been registered with the account."
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class DeployAction < CLI::Action
|
51
|
+
def steps
|
52
|
+
ensure_computer_is_registered
|
53
|
+
|
54
|
+
|
55
|
+
config = options[:config]
|
56
|
+
path = determine_path
|
57
|
+
|
58
|
+
if application_id = config[:application_id]
|
59
|
+
resource Application.get application_id, :lazy => true
|
60
|
+
else
|
61
|
+
resource Account.first.applications.new
|
62
|
+
end
|
63
|
+
|
64
|
+
resource[:path] = path
|
65
|
+
|
66
|
+
unless resource.persisted?
|
67
|
+
say "Please fill out the following information."
|
68
|
+
|
69
|
+
ask :name
|
70
|
+
ask :url
|
71
|
+
end
|
72
|
+
|
73
|
+
save do
|
74
|
+
config[:application_id] ||= resource[:id]
|
75
|
+
say "The application has successfully been deployed and is available at #{resource[:url]}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def determine_path
|
82
|
+
path = options[:path]
|
83
|
+
|
84
|
+
if File.exist?("#{path}/tmp/build")
|
85
|
+
return "#{path}/tmp/build"
|
86
|
+
end
|
87
|
+
|
88
|
+
path
|
89
|
+
end
|
90
|
+
|
91
|
+
def ensure_computer_is_registered
|
92
|
+
unless settings[:token]
|
93
|
+
say "This computer is not yet registered with a Strobe account."
|
94
|
+
|
95
|
+
choose do |m|
|
96
|
+
m.choice(:login) { run :login }
|
97
|
+
m.choice(:signup) { run :signup }
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Strobe
|
4
|
+
class CLI::Settings
|
5
|
+
def self.global_config_file
|
6
|
+
Pathname.new(Strobe.user_home).join(".strobe/config")
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.application_config_file(root)
|
10
|
+
Pathname.new(root).join(".strobe/config")
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(filename = self.class.global_config_file)
|
14
|
+
@filename = filename
|
15
|
+
|
16
|
+
if @filename.exist?
|
17
|
+
@hash = YAML.load(@filename)
|
18
|
+
else
|
19
|
+
@hash = {}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def [](k)
|
24
|
+
ENV[key(k)] || @hash[key(k)]
|
25
|
+
end
|
26
|
+
|
27
|
+
def []=(k, val)
|
28
|
+
@hash[key(k)] = val
|
29
|
+
|
30
|
+
# Persist the setting
|
31
|
+
FileUtils.mkdir_p(@filename.dirname)
|
32
|
+
File.open(@filename, "wb") do |file|
|
33
|
+
file.puts @hash.to_yaml
|
34
|
+
end
|
35
|
+
|
36
|
+
val
|
37
|
+
end
|
38
|
+
|
39
|
+
def connection
|
40
|
+
return @connection if @connection
|
41
|
+
host = self[:url] || "http://api.strobeapp.com/"
|
42
|
+
@connection = Connection.new(host)
|
43
|
+
end
|
44
|
+
|
45
|
+
def connect!
|
46
|
+
Strobe.connection = connection
|
47
|
+
authenticate!
|
48
|
+
end
|
49
|
+
|
50
|
+
def authenticate!
|
51
|
+
if token = self[:token]
|
52
|
+
connection.authenticate_with_token(token)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def key(key)
|
59
|
+
"STROBE_#{key}".upcase
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Strobe
|
2
|
+
class Collection
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
attr_reader :klass, :params
|
6
|
+
|
7
|
+
def initialize(klass, params = {})
|
8
|
+
@klass, @params = klass, params
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_a
|
12
|
+
@to_a ||= begin
|
13
|
+
resp = Strobe.connection.get klass.resource_uri, params
|
14
|
+
|
15
|
+
if resp.status == 200
|
16
|
+
resp.body[klass.resource_name].map do |hash|
|
17
|
+
klass.new(hash)
|
18
|
+
end
|
19
|
+
else
|
20
|
+
raise "Something went wrong"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def each
|
26
|
+
to_a.each { |r| yield r }
|
27
|
+
end
|
28
|
+
|
29
|
+
def all
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
def [](index)
|
34
|
+
to_a[index]
|
35
|
+
end
|
36
|
+
|
37
|
+
def length
|
38
|
+
to_a.length
|
39
|
+
end
|
40
|
+
|
41
|
+
def new(params = {})
|
42
|
+
klass.new(@params.merge(params))
|
43
|
+
end
|
44
|
+
|
45
|
+
def ==(other)
|
46
|
+
if Array === other
|
47
|
+
to_a === other
|
48
|
+
else
|
49
|
+
super
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'faraday'
|
3
|
+
|
4
|
+
module Strobe
|
5
|
+
class Connection
|
6
|
+
attr_reader :url
|
7
|
+
|
8
|
+
def initialize(url)
|
9
|
+
@url = url
|
10
|
+
@user = nil
|
11
|
+
@password = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def authenticate_with_token(token)
|
15
|
+
authenticate_with_user_and_password(token, '')
|
16
|
+
end
|
17
|
+
|
18
|
+
def authenticate_with_user_and_password(user, password)
|
19
|
+
@json_connection = nil
|
20
|
+
@reg_connection = nil
|
21
|
+
@user, @password = user, password
|
22
|
+
end
|
23
|
+
|
24
|
+
def get(*args)
|
25
|
+
request :get, *args
|
26
|
+
end
|
27
|
+
|
28
|
+
def post(*args)
|
29
|
+
request :post, *args
|
30
|
+
end
|
31
|
+
|
32
|
+
def put(*args)
|
33
|
+
request :put, *args
|
34
|
+
end
|
35
|
+
|
36
|
+
def delete(*args)
|
37
|
+
request :delete, *args
|
38
|
+
end
|
39
|
+
|
40
|
+
def request(method, path, body = '', headers = {})
|
41
|
+
connection = case headers['Content-Type']
|
42
|
+
when /^multipart\/form-data;/ then reg_connection
|
43
|
+
else json_connection
|
44
|
+
end
|
45
|
+
|
46
|
+
resp = connection.run_request method, path, body, headers
|
47
|
+
|
48
|
+
case resp.status
|
49
|
+
when 401
|
50
|
+
# TODO Fix this on the server
|
51
|
+
raise UnauthenticatedError, resp.body['errors']
|
52
|
+
when 404
|
53
|
+
raise ResourceNotFoundError, resp.body['errors']
|
54
|
+
end
|
55
|
+
|
56
|
+
resp
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def authorization_header
|
62
|
+
auth = Base64.encode64("#{@user}:#{@password}")
|
63
|
+
auth.gsub!("\n", "")
|
64
|
+
|
65
|
+
"Basic #{auth}"
|
66
|
+
end
|
67
|
+
|
68
|
+
def authorize?
|
69
|
+
@user
|
70
|
+
end
|
71
|
+
|
72
|
+
def json_connection
|
73
|
+
@json_connection ||= Faraday::Connection.new(:url => @url) do |b|
|
74
|
+
b.use Faraday::Request::ActiveSupportJson
|
75
|
+
b.use Faraday::Adapter::NetHttp
|
76
|
+
b.use Faraday::Response::ActiveSupportJson
|
77
|
+
b.options[:timeout] = 30
|
78
|
+
b.headers['authorization'] = authorization_header if authorize?
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def reg_connection
|
83
|
+
@reg_connection ||= Faraday::Connection.new(:url => @url) do |b|
|
84
|
+
b.use Faraday::Adapter::NetHttp
|
85
|
+
b.use Faraday::Response::ActiveSupportJson
|
86
|
+
b.options[:timeout] = 30
|
87
|
+
b.headers['authorization'] = authorization_header if authorize?
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,235 @@
|
|
1
|
+
module Strobe
|
2
|
+
module Resource
|
3
|
+
class AssociationValidator < ActiveModel::EachValidator
|
4
|
+
def validate_each(record, attr, val)
|
5
|
+
if inst = record.send(attr)
|
6
|
+
inst.valid?
|
7
|
+
inst.errors.each do |key, msg|
|
8
|
+
record.errors.add("#{attr}.#{key}", msg)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module Base
|
15
|
+
extend ActiveSupport::Concern
|
16
|
+
include Validations
|
17
|
+
|
18
|
+
included do
|
19
|
+
extend ActiveModel::Callbacks
|
20
|
+
define_model_callbacks :save, :create, :update, :destroy
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
def n
|
25
|
+
:n
|
26
|
+
end
|
27
|
+
|
28
|
+
def has(cardinality, name, options = {})
|
29
|
+
assoc = Association.new(self, cardinality, name, options)
|
30
|
+
|
31
|
+
class_eval(assoc.reader)
|
32
|
+
class_eval(assoc.writer)
|
33
|
+
|
34
|
+
if assoc.include?
|
35
|
+
validates_with AssociationValidator, :attributes => [name]
|
36
|
+
end
|
37
|
+
|
38
|
+
associations[assoc.name] = assoc
|
39
|
+
end
|
40
|
+
|
41
|
+
def filter(*params)
|
42
|
+
@filter ||= []
|
43
|
+
|
44
|
+
return @filter if params.empty?
|
45
|
+
|
46
|
+
@filter |= params.map { |p| p.to_s }
|
47
|
+
end
|
48
|
+
|
49
|
+
def associations
|
50
|
+
@associations ||= {}
|
51
|
+
end
|
52
|
+
|
53
|
+
def base_const_name
|
54
|
+
name.gsub(/^Strobe::Resources::/, '')
|
55
|
+
end
|
56
|
+
|
57
|
+
def resource_name
|
58
|
+
raise NotImplementedError, 'Implemented in a subclass'
|
59
|
+
end
|
60
|
+
|
61
|
+
def singular_resource_name
|
62
|
+
raise NotImplementedError, 'Implemented in a subclass'
|
63
|
+
end
|
64
|
+
|
65
|
+
def uri_prefix
|
66
|
+
@uri_prefix ||= "/#{resource_name}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
attr_reader :response
|
71
|
+
|
72
|
+
def initialize(params = {})
|
73
|
+
@response = nil
|
74
|
+
self.params = params || {}
|
75
|
+
end
|
76
|
+
|
77
|
+
def persisted?
|
78
|
+
self[:id]
|
79
|
+
end
|
80
|
+
|
81
|
+
# TODO: Make this legit
|
82
|
+
def key?(key)
|
83
|
+
params.key?(key)
|
84
|
+
end
|
85
|
+
|
86
|
+
def params
|
87
|
+
@params.dup.tap do |params|
|
88
|
+
self.class.associations.each do |name, assoc|
|
89
|
+
next unless assoc.include? and inst = send(name)
|
90
|
+
params.deep_merge! name => inst.params
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def params=(val)
|
96
|
+
params = with_indifferent_access(val)
|
97
|
+
|
98
|
+
extract_on_associations(params).each do |k, v|
|
99
|
+
send("#{k}=", v)
|
100
|
+
end
|
101
|
+
|
102
|
+
@params = params
|
103
|
+
val
|
104
|
+
end
|
105
|
+
|
106
|
+
def [](key)
|
107
|
+
if Array === key
|
108
|
+
parts = key
|
109
|
+
else
|
110
|
+
parts = key.to_s.split('.')
|
111
|
+
end
|
112
|
+
|
113
|
+
if self.class.associations.key?(parts.first.to_sym)
|
114
|
+
return unless inst = send(parts.first)
|
115
|
+
return inst if parts.length == 1
|
116
|
+
inst[parts[1..-1]]
|
117
|
+
else
|
118
|
+
parts.inject(params) do |param, key|
|
119
|
+
param[key]
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def []=(key, val)
|
125
|
+
merge!(key.to_s.split('.').reverse.inject(val) do |param, key|
|
126
|
+
{ key => param }
|
127
|
+
end)
|
128
|
+
end
|
129
|
+
|
130
|
+
def merge!(params)
|
131
|
+
params = with_indifferent_access(params || {})
|
132
|
+
|
133
|
+
extract_on_associations(params).each do |k, v|
|
134
|
+
unless inst = send(k)
|
135
|
+
inst = send("#{k}=", {})
|
136
|
+
end
|
137
|
+
inst.merge! v
|
138
|
+
end
|
139
|
+
|
140
|
+
@params.deep_merge! params
|
141
|
+
end
|
142
|
+
|
143
|
+
def persisted?
|
144
|
+
self[:id]
|
145
|
+
end
|
146
|
+
|
147
|
+
def save
|
148
|
+
if valid?
|
149
|
+
root = self.class.singular_resource_name
|
150
|
+
@response = self["id"] ? update : create
|
151
|
+
|
152
|
+
if @response.success?
|
153
|
+
merge! @response.body[root]
|
154
|
+
return true
|
155
|
+
elsif @response.status == 422
|
156
|
+
errors = @response.body['errors']
|
157
|
+
errors = errors[root] if errors
|
158
|
+
handle_errors(errors) if errors
|
159
|
+
else
|
160
|
+
raise "Something went wrong"
|
161
|
+
end
|
162
|
+
|
163
|
+
false
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def destroy
|
168
|
+
raise NotImplemented
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
def connection
|
174
|
+
Strobe.connection
|
175
|
+
end
|
176
|
+
|
177
|
+
def http_verb
|
178
|
+
persisted? ? :put : :post
|
179
|
+
end
|
180
|
+
|
181
|
+
def http_uri
|
182
|
+
uri = self.class.uri_prefix
|
183
|
+
uri = "#{uri}/#{self[:id]}" if self[:id]
|
184
|
+
uri
|
185
|
+
end
|
186
|
+
|
187
|
+
def request_params
|
188
|
+
filter = self.class.filter
|
189
|
+
{ self.class.singular_resource_name => params.except(*filter) }
|
190
|
+
end
|
191
|
+
|
192
|
+
def create
|
193
|
+
connection.post http_uri, request_params, {}
|
194
|
+
end
|
195
|
+
|
196
|
+
def update
|
197
|
+
connection.put http_uri, request_params, {}
|
198
|
+
end
|
199
|
+
|
200
|
+
def handle_errors(errors, key = nil)
|
201
|
+
errors.each do |k, v|
|
202
|
+
key = key ? "#{key}.#{k}" : k
|
203
|
+
case v
|
204
|
+
when Hash
|
205
|
+
handle_errors(v, key)
|
206
|
+
else
|
207
|
+
self.errors.add(key, v)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def with_indifferent_access(val)
|
213
|
+
case val
|
214
|
+
when Hash
|
215
|
+
h = HashWithIndifferentAccess.new
|
216
|
+
val.each { |k, v| h[k] = with_indifferent_access(v) }
|
217
|
+
h
|
218
|
+
when Array
|
219
|
+
val.map { |elem| with_indifferent_access(elem) }
|
220
|
+
else
|
221
|
+
val
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def extract_on_associations(params)
|
226
|
+
{}.tap do |ret|
|
227
|
+
self.class.associations.keys.each do |key|
|
228
|
+
next unless params.key?(key)
|
229
|
+
ret[key] = params.delete(key)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|