strobe 0.0.2.1 → 0.1.0.beta.1
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/strobe +1 -1
- data/lib/strobe/association.rb +2 -1
- data/lib/strobe/cli/main.rb +167 -0
- data/lib/strobe/cli/settings.rb +9 -1
- data/lib/strobe/cli/table.rb +51 -0
- data/lib/strobe/cli/users.rb +65 -0
- data/lib/strobe/cli.rb +168 -32
- data/lib/strobe/collection.rb +13 -0
- data/lib/strobe/connection.rb +6 -3
- data/lib/strobe/identity_map.rb +48 -0
- data/lib/strobe/key.rb +21 -0
- data/lib/strobe/resource/base.rb +37 -11
- data/lib/strobe/resource/collection.rb +58 -8
- data/lib/strobe/resources/account.rb +2 -1
- data/lib/strobe/resources/application.rb +8 -4
- data/lib/strobe/resources/assignment.rb +12 -0
- data/lib/strobe/resources/membership.rb +13 -0
- data/lib/strobe/resources/team.rb +3 -1
- data/lib/strobe/resources/user.rb +2 -0
- data/lib/strobe/validations.rb +9 -1
- data/lib/strobe.rb +5 -0
- metadata +12 -6
- data/lib/strobe/cli/action.rb +0 -131
- data/lib/strobe/cli/actions.rb +0 -104
data/bin/strobe
CHANGED
data/lib/strobe/association.rb
CHANGED
@@ -43,7 +43,7 @@ module Strobe
|
|
43
43
|
|
44
44
|
ruby << " return @__#{name} if @__#{name}"
|
45
45
|
ruby << " return nil unless self[:#{name}_id]"
|
46
|
-
ruby << " @__#{name} = #{target_name}.get( self[:#{name}_id] )"
|
46
|
+
ruby << " @__#{name} = #{target_name}.get!( self[:#{name}_id] )"
|
47
47
|
|
48
48
|
end
|
49
49
|
|
@@ -80,6 +80,7 @@ module Strobe
|
|
80
80
|
end
|
81
81
|
|
82
82
|
ruby << "end"
|
83
|
+
|
83
84
|
ruby.join("\n")
|
84
85
|
end
|
85
86
|
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
module Strobe
|
2
|
+
class CLI::Main < CLI
|
3
|
+
|
4
|
+
action "users", 'manage users' do |*args|
|
5
|
+
argv = $ARGV[1..-1]
|
6
|
+
argv = [ 'list' ] + argv if args.empty?
|
7
|
+
CLI::Users.start(argv)
|
8
|
+
end
|
9
|
+
|
10
|
+
action "signup", "signup for a new Strobe account" do
|
11
|
+
resource Signup.new :account => { :name => 'default' }
|
12
|
+
|
13
|
+
if settings[:token]
|
14
|
+
say "This computer is already registered with a Strobe account."
|
15
|
+
unless agree "Signup anyway? [Yn] "
|
16
|
+
say "exiting..."
|
17
|
+
exit
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
until_success do
|
22
|
+
say "Please fill out the following information."
|
23
|
+
|
24
|
+
ask :invitation
|
25
|
+
ask :email, :into => "user.email"
|
26
|
+
|
27
|
+
group :validate => "user.password" do
|
28
|
+
password :password, :into => "user.password"
|
29
|
+
password :password_confirmation, :into => "password_confirmation"
|
30
|
+
end
|
31
|
+
|
32
|
+
save do
|
33
|
+
settings[:token] = resource['user.authentication_token']
|
34
|
+
settings.authenticate!
|
35
|
+
say "Your account was created successfully."
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
action "login", "register this computer with an existing Strobe account" do
|
41
|
+
resource Me.new
|
42
|
+
|
43
|
+
until_success do
|
44
|
+
say "Please enter your Strobe username and password."
|
45
|
+
|
46
|
+
ask :email, :into => "user.email"
|
47
|
+
password :password, :into => "user.password"
|
48
|
+
|
49
|
+
save do
|
50
|
+
settings[:token] = resource['user.authentication_token']
|
51
|
+
settings.authenticate!
|
52
|
+
say "The computer has been registered with the account."
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
application_path_option
|
58
|
+
method_option :staging, :type => :boolean, :banner => "deploy to a staging environment"
|
59
|
+
action "deploy", "deploy your application to Strobe" do
|
60
|
+
ensure_computer_is_registered
|
61
|
+
|
62
|
+
if application_id = config[:application_id]
|
63
|
+
resource Application.get! application_id, :lazy => true
|
64
|
+
else
|
65
|
+
resource Account.first.applications.new
|
66
|
+
end
|
67
|
+
|
68
|
+
resource[:path] = determine_application_root
|
69
|
+
|
70
|
+
unless resource.persisted?
|
71
|
+
say "Please fill out the following information."
|
72
|
+
|
73
|
+
ask :name
|
74
|
+
|
75
|
+
save do
|
76
|
+
config[:application_id] ||= resource[:id]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
host = resource.deploy! :environment => options[:staging] && 'staging'
|
81
|
+
say "The application has successfully been deployed and is available at #{host}"
|
82
|
+
end
|
83
|
+
|
84
|
+
action "applications", "list all of your applications" do
|
85
|
+
empty = "You do not have any applications. Try deploying one with `strobe deploy`."
|
86
|
+
table Resources::Application.all, :empty => empty do |t|
|
87
|
+
t.column :id
|
88
|
+
t.column :name
|
89
|
+
t.column :url
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
method_option "url", :type => :string
|
94
|
+
application_path_option
|
95
|
+
action "set", "update the settings for the application" do
|
96
|
+
resource get_application
|
97
|
+
resource['url'] = options['url']
|
98
|
+
|
99
|
+
save do
|
100
|
+
say "The application is now running at #{resource[:url]}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
application_path_option
|
105
|
+
action "delete", "delete a specific application" do
|
106
|
+
resource get_application
|
107
|
+
|
108
|
+
say "Deleting application '#{resource[:name]}'"
|
109
|
+
|
110
|
+
unless agree "Are you sure? [Yn] "
|
111
|
+
say "Aborting..."
|
112
|
+
exit
|
113
|
+
end
|
114
|
+
|
115
|
+
delete do
|
116
|
+
config.delete
|
117
|
+
say "The application has been deleted"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
application_path_option
|
122
|
+
method_option "id", :type => :numeric, :banner => "The ID of the remote application"
|
123
|
+
action "register", "registers a local application with an existing Strobe application" do
|
124
|
+
if config[:application_id] && app = Application.get(config[:application_id])
|
125
|
+
say "The directory is already registered with the application `#{app[:name]}`"
|
126
|
+
unless agree "Continue? [Yn] "
|
127
|
+
say "Aborting..."
|
128
|
+
end
|
129
|
+
config.delete
|
130
|
+
end
|
131
|
+
|
132
|
+
application = Application.get! options[:id]
|
133
|
+
config[:application_id] = application[:id]
|
134
|
+
say "You can now deploy to #{application[:name]} (http://#{application[:url]})"
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def determine_application_root
|
140
|
+
if File.exist?("#{path}/tmp/build")
|
141
|
+
return "#{path}/tmp/build"
|
142
|
+
end
|
143
|
+
|
144
|
+
path
|
145
|
+
end
|
146
|
+
|
147
|
+
def ensure_computer_is_registered
|
148
|
+
unless settings[:token]
|
149
|
+
say "This computer is not yet registered with a Strobe account."
|
150
|
+
|
151
|
+
choose do |m|
|
152
|
+
m.choice(:login) { invoke :login }
|
153
|
+
m.choice(:signup) { invoke :signup }
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def get_application(opts = {})
|
159
|
+
unless id = config[:application_id]
|
160
|
+
error "No application found. Are you currently in the correct directory?"
|
161
|
+
exit 1
|
162
|
+
end
|
163
|
+
|
164
|
+
Application.get!(id, opts)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
data/lib/strobe/cli/settings.rb
CHANGED
@@ -12,11 +12,15 @@ module Strobe
|
|
12
12
|
Pathname.new(root).join(".strobe/#{file}")
|
13
13
|
end
|
14
14
|
|
15
|
+
def self.application(root)
|
16
|
+
new application_config_file(root)
|
17
|
+
end
|
18
|
+
|
15
19
|
def initialize(filename = self.class.global_config_file)
|
16
20
|
@filename = filename
|
17
21
|
|
18
22
|
if @filename.exist?
|
19
|
-
@hash = YAML.load(@filename)
|
23
|
+
@hash = YAML.load(@filename) || {}
|
20
24
|
else
|
21
25
|
@hash = {}
|
22
26
|
end
|
@@ -55,6 +59,10 @@ module Strobe
|
|
55
59
|
end
|
56
60
|
end
|
57
61
|
|
62
|
+
def delete
|
63
|
+
FileUtils.rm_f(@filename)
|
64
|
+
end
|
65
|
+
|
58
66
|
private
|
59
67
|
|
60
68
|
def key(key)
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Strobe
|
2
|
+
class CLI::Table
|
3
|
+
def initialize(collection, opts = {})
|
4
|
+
@collection = collection
|
5
|
+
@columns = []
|
6
|
+
@computed = []
|
7
|
+
@opts = opts
|
8
|
+
end
|
9
|
+
|
10
|
+
def column(key)
|
11
|
+
@columns << { :key => key, :max => 0 }
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
@collection.each do |obj|
|
16
|
+
@computed << @columns.map do |col|
|
17
|
+
cell = obj[ col[:key] ].to_s
|
18
|
+
col[:max] = [ cell.length, col[:max] ].max
|
19
|
+
cell
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
if @computed.empty?
|
24
|
+
return @opts[:empty] || "Nothing to see here..."
|
25
|
+
end
|
26
|
+
|
27
|
+
str = ""
|
28
|
+
|
29
|
+
@columns.each do |col|
|
30
|
+
str << cell_to_s(col, col[:key].to_s.upcase)
|
31
|
+
end
|
32
|
+
|
33
|
+
str << "\n"
|
34
|
+
|
35
|
+
@computed.each do |cells|
|
36
|
+
cells.each_with_index do |cell, i|
|
37
|
+
str << cell_to_s(@columns[i], cell)
|
38
|
+
end
|
39
|
+
str << "\n"
|
40
|
+
end
|
41
|
+
str
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def cell_to_s(col, cell)
|
47
|
+
width = [ col[:max], 25 ].max + 5
|
48
|
+
cell.ljust(width)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Strobe
|
2
|
+
class CLI::Users < CLI
|
3
|
+
|
4
|
+
application_path_option
|
5
|
+
action "list", "list users that have access to this application" do
|
6
|
+
@team = get_current_team!
|
7
|
+
if @team.memberships.any?
|
8
|
+
say "The following users also have access to the application"
|
9
|
+
say
|
10
|
+
|
11
|
+
@team.memberships.each do |m|
|
12
|
+
email = m.user ? m.user[:email] : m[:email]
|
13
|
+
say " * #{email}"
|
14
|
+
end
|
15
|
+
else
|
16
|
+
say "You are the only user with access to this application"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
application_path_option
|
21
|
+
action "add", "give a user access to this application" do |email|
|
22
|
+
@team = get_current_team!
|
23
|
+
@team.memberships.create! :email => email
|
24
|
+
|
25
|
+
say "`#{email}` has been invited to the application"
|
26
|
+
end
|
27
|
+
|
28
|
+
application_path_option
|
29
|
+
action "remove", "remove a user's access from this application" do |email|
|
30
|
+
@team = get_current_team!
|
31
|
+
@team.memberships.each do |m|
|
32
|
+
m.destroy if (m.user ? m.user[:email] : m[:email]) == email
|
33
|
+
end
|
34
|
+
|
35
|
+
say "`#{email}` has been removed from the application"
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def get_current_team!
|
41
|
+
if team = current_application.teams.first
|
42
|
+
team
|
43
|
+
else
|
44
|
+
# Create a team for the application
|
45
|
+
account = current_application.account
|
46
|
+
name = "Developers for #{current_application[:name]}"
|
47
|
+
team = Team.create! :name => name, :account => account
|
48
|
+
|
49
|
+
team.assignments.create! :application => current_application
|
50
|
+
team
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def current_application
|
55
|
+
return @current_application if @current_application
|
56
|
+
|
57
|
+
unless id = config[:application_id]
|
58
|
+
error "No application found. Are you currently in the correct directory?"
|
59
|
+
exit 1
|
60
|
+
end
|
61
|
+
|
62
|
+
@current_application = Application.get!(config[:application_id])
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/strobe/cli.rb
CHANGED
@@ -1,65 +1,201 @@
|
|
1
1
|
require 'thor'
|
2
2
|
require 'pathname'
|
3
3
|
require 'fileutils'
|
4
|
+
require 'highline'
|
4
5
|
|
5
6
|
module Strobe
|
6
7
|
class CLI < Thor
|
7
|
-
autoload :
|
8
|
-
autoload :Settings,
|
8
|
+
autoload :Main, 'strobe/cli/main'
|
9
|
+
autoload :Settings, 'strobe/cli/settings'
|
10
|
+
autoload :Table, 'strobe/cli/table'
|
11
|
+
autoload :Users, 'strobe/cli/users'
|
9
12
|
|
10
|
-
|
11
|
-
|
13
|
+
include Resources
|
14
|
+
|
15
|
+
class_option "backtrace", :type => :boolean
|
16
|
+
|
17
|
+
def self.start(args)
|
18
|
+
$ARGV = args
|
19
|
+
IdentityMap.wrap do
|
20
|
+
super
|
21
|
+
end
|
12
22
|
rescue Strobe::StrobeError => e
|
13
|
-
raise if $
|
23
|
+
raise if $ARGV.include?('--backtrace')
|
14
24
|
abort "[ERROR] #{e.message}"
|
15
25
|
end
|
16
26
|
|
17
|
-
|
27
|
+
def self.action(name, *args, &blk)
|
28
|
+
@haxliases ||= []
|
29
|
+
@haxliases << name.to_s
|
30
|
+
desc name, *args
|
31
|
+
define_method("__hax__#{name}", &blk)
|
32
|
+
map name => "__hax__#{name}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.__haxlias(name)
|
36
|
+
@haxliases ||= []
|
37
|
+
|
38
|
+
return name if Class === name
|
39
|
+
|
40
|
+
if @haxliases.include?(name.to_s)
|
41
|
+
"__hax__#{name}"
|
42
|
+
else
|
43
|
+
name
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.application_path_option
|
48
|
+
method_option :path, :type => :string, :banner => "Path to your application"
|
49
|
+
end
|
18
50
|
|
19
51
|
def initialize(*)
|
20
52
|
super
|
21
53
|
|
22
|
-
|
23
|
-
|
54
|
+
@resource = nil
|
55
|
+
@highline = HighLine.new
|
56
|
+
|
57
|
+
# State
|
58
|
+
@in_group = false
|
59
|
+
@success = false
|
60
|
+
|
61
|
+
settings.connect!
|
24
62
|
end
|
25
63
|
|
26
|
-
|
27
|
-
require "strobe/cli/actions"
|
64
|
+
private
|
28
65
|
|
29
|
-
|
30
|
-
|
31
|
-
_run :signup
|
66
|
+
def path
|
67
|
+
options[:path] || Dir.pwd
|
32
68
|
end
|
33
69
|
|
34
|
-
|
35
|
-
|
36
|
-
_run :login
|
70
|
+
def config
|
71
|
+
@config ||= Settings.application(path)
|
37
72
|
end
|
38
73
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
file = ENV['STROBE_CONFIG'] || 'config'
|
44
|
-
config = Settings.new(Pathname.new(path).join(".strobe/#{file}"))
|
74
|
+
def invoke(action, *args)
|
75
|
+
action = self.class.__haxlias(action)
|
76
|
+
super
|
77
|
+
end
|
45
78
|
|
46
|
-
|
79
|
+
def resource(resource = nil)
|
80
|
+
@resource = resource if resource
|
81
|
+
@resource
|
47
82
|
end
|
48
83
|
|
49
|
-
|
84
|
+
def settings
|
85
|
+
@settings ||= Settings.new
|
86
|
+
end
|
50
87
|
|
51
|
-
def
|
52
|
-
|
53
|
-
|
54
|
-
|
88
|
+
def until_success(&blk)
|
89
|
+
until @success
|
90
|
+
retval = blk.call
|
91
|
+
end
|
92
|
+
@success = false
|
93
|
+
retval
|
55
94
|
end
|
56
95
|
|
57
|
-
def
|
58
|
-
|
96
|
+
def save(opts = {})
|
97
|
+
if resource.save
|
98
|
+
@success = true
|
99
|
+
yield
|
100
|
+
else
|
101
|
+
display_errors
|
102
|
+
end
|
59
103
|
end
|
60
104
|
|
61
|
-
def
|
62
|
-
|
105
|
+
def delete(opts = {})
|
106
|
+
if resource.destroy
|
107
|
+
@success = true
|
108
|
+
yield
|
109
|
+
else
|
110
|
+
display_errors
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def say(what = nil)
|
115
|
+
puts what
|
116
|
+
end
|
117
|
+
|
118
|
+
def error(what)
|
119
|
+
STDERR.puts "[ERROR] #{what}"
|
120
|
+
end
|
121
|
+
|
122
|
+
def agree(msg, *args)
|
123
|
+
@highline.agree(msg, *args) do |q|
|
124
|
+
next unless STDIN.tty? # For testing
|
125
|
+
q.readline = true
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def ask(key, options = {})
|
130
|
+
input key, options do |msg|
|
131
|
+
resp = @highline.ask msg do |q|
|
132
|
+
next unless STDIN.tty? # For testing
|
133
|
+
q.readline = true
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def password(key, options = {})
|
139
|
+
input key, options do |msg|
|
140
|
+
@highline.ask msg do |q|
|
141
|
+
next unless STDIN.tty? # For testing
|
142
|
+
q.echo = '*'
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def choose
|
148
|
+
@highline.choose do |menu|
|
149
|
+
yield menu
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def group(options = {}, &blk)
|
154
|
+
case
|
155
|
+
when key = options[:validate]
|
156
|
+
validation_group(key, &blk)
|
157
|
+
else
|
158
|
+
yield if block_given?
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def input(key, options)
|
163
|
+
into = options[:into] || key
|
164
|
+
|
165
|
+
validation_group into do
|
166
|
+
msg = (key.to_s.humanize + ': ').ljust(25)
|
167
|
+
resource[into] = yield msg
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def validation_group(resource_key)
|
172
|
+
return yield if @in_group
|
173
|
+
|
174
|
+
begin
|
175
|
+
@in_group = true
|
176
|
+
while true
|
177
|
+
yield
|
178
|
+
|
179
|
+
break if resource.valid_attribute?(resource_key)
|
180
|
+
|
181
|
+
display_errors
|
182
|
+
end
|
183
|
+
ensure
|
184
|
+
@in_group = false
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def display_errors
|
189
|
+
resource.errors.each do |k, msg|
|
190
|
+
key = k.to_s.split('.').last
|
191
|
+
say "[ERROR] #{key.humanize} #{msg}"
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def table(collection, opts = {})
|
196
|
+
table = Table.new(collection, opts)
|
197
|
+
yield table
|
198
|
+
puts table.to_s
|
63
199
|
end
|
64
200
|
end
|
65
201
|
end
|
data/lib/strobe/collection.rb
CHANGED
@@ -11,6 +11,7 @@ module Strobe
|
|
11
11
|
def to_a
|
12
12
|
@to_a ||= begin
|
13
13
|
resp = Strobe.connection.get klass.resource_uri, params
|
14
|
+
resp.validate!
|
14
15
|
|
15
16
|
if resp.status == 200
|
16
17
|
resp.body[klass.resource_name].map do |hash|
|
@@ -42,6 +43,14 @@ module Strobe
|
|
42
43
|
klass.new(@params.merge(params))
|
43
44
|
end
|
44
45
|
|
46
|
+
def create(params = {})
|
47
|
+
klass.create(@params.merge(params))
|
48
|
+
end
|
49
|
+
|
50
|
+
def create!(params = {})
|
51
|
+
klass.create!(@params.merge(params))
|
52
|
+
end
|
53
|
+
|
45
54
|
def ==(other)
|
46
55
|
if Array === other
|
47
56
|
to_a === other
|
@@ -49,5 +58,9 @@ module Strobe
|
|
49
58
|
super
|
50
59
|
end
|
51
60
|
end
|
61
|
+
|
62
|
+
def reload!
|
63
|
+
@to_a = nil
|
64
|
+
end
|
52
65
|
end
|
53
66
|
end
|
data/lib/strobe/connection.rb
CHANGED
@@ -71,12 +71,15 @@ module Strobe
|
|
71
71
|
end
|
72
72
|
|
73
73
|
def validate!
|
74
|
+
msg = body['errors'] && body['errors']['request']
|
74
75
|
case status
|
75
76
|
when 401
|
76
77
|
# TODO Fix this on the server
|
77
|
-
raise UnauthenticatedError,
|
78
|
+
raise UnauthenticatedError, msg
|
78
79
|
when 404
|
79
|
-
raise ResourceNotFoundError,
|
80
|
+
raise ResourceNotFoundError, msg
|
81
|
+
when 500...600
|
82
|
+
raise ServerError, "The server puked :(. Don't worry, the error has been reported."
|
80
83
|
end
|
81
84
|
|
82
85
|
self
|
@@ -119,7 +122,7 @@ module Strobe
|
|
119
122
|
request = Net::HTTPGenericRequest.new(
|
120
123
|
method.to_s.upcase, !!body, true, path, headers)
|
121
124
|
|
122
|
-
Response.new(http.request(request, body))
|
125
|
+
Response.new(http.request(request, body))
|
123
126
|
end
|
124
127
|
|
125
128
|
private
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Strobe
|
2
|
+
class IdentityMap
|
3
|
+
def self.wrap
|
4
|
+
Thread.current[:__strobe_im] = new
|
5
|
+
yield
|
6
|
+
ensure
|
7
|
+
Thread.current[:__strobe_im] = nil
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.identify(klass, key)
|
11
|
+
return yield unless current && key
|
12
|
+
|
13
|
+
inst = current[klass, key]
|
14
|
+
|
15
|
+
return inst if inst
|
16
|
+
|
17
|
+
current[klass, key] = yield if block_given?
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.forget(klass, key)
|
21
|
+
current && key && current[klass, key] = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.reset!
|
25
|
+
current && current.reset!
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.current
|
29
|
+
Thread.current[:__strobe_im]
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize
|
33
|
+
reset!
|
34
|
+
end
|
35
|
+
|
36
|
+
def [](klass, key)
|
37
|
+
@map[klass.to_s][klass.key.typecast(key)]
|
38
|
+
end
|
39
|
+
|
40
|
+
def []=(klass, key, val)
|
41
|
+
@map[klass.to_s][klass.key.typecast(key)] = val
|
42
|
+
end
|
43
|
+
|
44
|
+
def reset!
|
45
|
+
@map = Hash.new { |h,k| h[k] = {} }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/strobe/key.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Strobe
|
2
|
+
class Key
|
3
|
+
attr_reader :property, :type
|
4
|
+
|
5
|
+
def initialize(property, type)
|
6
|
+
@property = property.to_s
|
7
|
+
@type = type
|
8
|
+
end
|
9
|
+
|
10
|
+
def key_for(obj)
|
11
|
+
typecast(obj[property])
|
12
|
+
end
|
13
|
+
|
14
|
+
def typecast(val)
|
15
|
+
return if val.nil?
|
16
|
+
return val.to_s if type == String
|
17
|
+
return val.to_i if type == Integer
|
18
|
+
raise "Unknown key type `#{type}`"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/strobe/resource/base.rb
CHANGED
@@ -1,4 +1,7 @@
|
|
1
1
|
module Strobe
|
2
|
+
class RequestError < RuntimeError ; end
|
3
|
+
class ValidationError < RuntimeError ; end
|
4
|
+
|
2
5
|
module Resource
|
3
6
|
class AssociationValidator < ActiveModel::EachValidator
|
4
7
|
def validate_each(record, attr, val)
|
@@ -71,6 +74,7 @@ module Strobe
|
|
71
74
|
|
72
75
|
def initialize(params = {})
|
73
76
|
@response = nil
|
77
|
+
@params = with_indifferent_access({})
|
74
78
|
self.params = params || {}
|
75
79
|
end
|
76
80
|
|
@@ -93,13 +97,8 @@ module Strobe
|
|
93
97
|
end
|
94
98
|
|
95
99
|
def params=(val)
|
96
|
-
params = with_indifferent_access(
|
97
|
-
|
98
|
-
extract_on_associations(params).each do |k, v|
|
99
|
-
send("#{k}=", v)
|
100
|
-
end
|
101
|
-
|
102
|
-
@params = params
|
100
|
+
@params = with_indifferent_access({})
|
101
|
+
merge!(val)
|
103
102
|
val
|
104
103
|
end
|
105
104
|
|
@@ -131,10 +130,13 @@ module Strobe
|
|
131
130
|
params = with_indifferent_access(params || {})
|
132
131
|
|
133
132
|
extract_on_associations(params).each do |k, v|
|
134
|
-
|
135
|
-
|
133
|
+
if Base === v
|
134
|
+
send("#{k}=", v)
|
135
|
+
elsif inst = send(k)
|
136
|
+
inst.merge!(v)
|
137
|
+
else
|
138
|
+
send("#{k}=", v)
|
136
139
|
end
|
137
|
-
inst.merge! v
|
138
140
|
end
|
139
141
|
|
140
142
|
@params.deep_merge! params
|
@@ -150,8 +152,15 @@ module Strobe
|
|
150
152
|
end
|
151
153
|
end
|
152
154
|
|
155
|
+
def save!
|
156
|
+
save or raise ValidationError, "could not validate: #{errors.full_messages}"
|
157
|
+
end
|
158
|
+
|
153
159
|
def destroy
|
154
|
-
|
160
|
+
if request { delete }
|
161
|
+
IdentityMap.forget(self.class, key)
|
162
|
+
true
|
163
|
+
end
|
155
164
|
end
|
156
165
|
|
157
166
|
private
|
@@ -174,11 +183,24 @@ module Strobe
|
|
174
183
|
@response = yield
|
175
184
|
root = self.class.singular_resource_name
|
176
185
|
|
186
|
+
@response.validate!
|
187
|
+
|
177
188
|
if @response.success?
|
178
189
|
merge! @response.body[root]
|
190
|
+
|
191
|
+
# register with the IM
|
192
|
+
IdentityMap.identify(self.class, self[:id]) do
|
193
|
+
self
|
194
|
+
end
|
195
|
+
|
179
196
|
return true
|
180
197
|
elsif @response.status == 422
|
181
198
|
errors = @response.body['errors']
|
199
|
+
|
200
|
+
if msg = errors['request']
|
201
|
+
raise RequestError, msg
|
202
|
+
end
|
203
|
+
|
182
204
|
errors = errors[root] if errors
|
183
205
|
handle_errors(errors) if errors
|
184
206
|
else
|
@@ -201,6 +223,10 @@ module Strobe
|
|
201
223
|
connection.put http_uri, request_params, {}
|
202
224
|
end
|
203
225
|
|
226
|
+
def delete
|
227
|
+
connection.delete http_uri
|
228
|
+
end
|
229
|
+
|
204
230
|
def handle_errors(errors, key = nil)
|
205
231
|
errors.each do |k, v|
|
206
232
|
key = key ? "#{key}.#{k}" : k
|
@@ -7,6 +7,14 @@ module Strobe
|
|
7
7
|
module ClassMethods
|
8
8
|
include Enumerable
|
9
9
|
|
10
|
+
def key(property = nil, type = Integer)
|
11
|
+
@key ||= Key.new(:id, Integer)
|
12
|
+
|
13
|
+
return @key unless property
|
14
|
+
|
15
|
+
@key = Key.new(property, type)
|
16
|
+
end
|
17
|
+
|
10
18
|
def each
|
11
19
|
collection.each { |r| yield r }
|
12
20
|
end
|
@@ -29,21 +37,63 @@ module Strobe
|
|
29
37
|
|
30
38
|
alias all collection
|
31
39
|
|
32
|
-
def
|
33
|
-
inst =
|
34
|
-
|
40
|
+
def new(params = {})
|
41
|
+
inst = IdentityMap.identify(self, key.key_for(params)) do
|
42
|
+
super({})
|
43
|
+
end
|
44
|
+
inst.merge!(params)
|
35
45
|
inst
|
36
46
|
end
|
47
|
+
|
48
|
+
def create(params = {})
|
49
|
+
new(params).tap { |inst| inst.save }
|
50
|
+
end
|
51
|
+
|
52
|
+
def create!(params = {})
|
53
|
+
new(params).tap { |inst| inst.save! }
|
54
|
+
end
|
55
|
+
|
56
|
+
def get(id, opts = {})
|
57
|
+
return nil unless id
|
58
|
+
|
59
|
+
IdentityMap.identify self, id do
|
60
|
+
inst = new :id => id
|
61
|
+
break inst if opts[:lazy]
|
62
|
+
inst.reload(opts) && inst
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def get!(id, opts = {})
|
67
|
+
raise ResourceNotFoundError, "can not find resource will nil key" unless id
|
68
|
+
|
69
|
+
IdentityMap.identify self, id do
|
70
|
+
inst = new :id => id
|
71
|
+
inst.reload!
|
72
|
+
inst
|
73
|
+
end
|
74
|
+
end
|
37
75
|
end
|
38
76
|
|
39
77
|
def id
|
40
|
-
|
78
|
+
raise NotImplementedError
|
41
79
|
end
|
42
80
|
|
43
|
-
def
|
44
|
-
|
45
|
-
|
46
|
-
|
81
|
+
def key
|
82
|
+
self.class.key.key_for(self)
|
83
|
+
end
|
84
|
+
|
85
|
+
def reload!(opts = {})
|
86
|
+
reload(opts.merge(:validate => true))
|
87
|
+
end
|
88
|
+
|
89
|
+
def reload(opts = {})
|
90
|
+
resp = Strobe.connection.get "#{self.class.resource_uri}/#{key}"
|
91
|
+
resp.validate! if opts[:validate]
|
92
|
+
|
93
|
+
if resp.success?
|
94
|
+
self.params = resp.body[self.class.singular_resource_name]
|
95
|
+
self
|
96
|
+
end
|
47
97
|
end
|
48
98
|
end
|
49
99
|
end
|
@@ -12,17 +12,21 @@ module Strobe
|
|
12
12
|
|
13
13
|
filter :path
|
14
14
|
|
15
|
-
validates "account", "name",
|
15
|
+
validates "account", "name", :presence => true
|
16
16
|
|
17
|
-
def deploy!(
|
18
|
-
self['path'] = path if path
|
17
|
+
def deploy!(opts = {})
|
18
|
+
self['path'] = opts[:path] if opts[:path]
|
19
|
+
environment = opts[:environment]
|
19
20
|
|
20
21
|
validate_for_deploy or return
|
21
22
|
|
22
23
|
request do
|
24
|
+
qs = "?environment=#{environment}" if environment
|
23
25
|
packfile = build_packfile
|
24
|
-
connection.put("#{http_uri}/deploy", packfile.to_s, packfile.headers)
|
26
|
+
connection.put("#{http_uri}/deploy#{qs}", packfile.to_s, packfile.headers)
|
25
27
|
end
|
28
|
+
|
29
|
+
[ environment, self['url'] ].compact.join('.')
|
26
30
|
end
|
27
31
|
|
28
32
|
private
|
data/lib/strobe/validations.rb
CHANGED
@@ -67,11 +67,15 @@ module Strobe
|
|
67
67
|
def valid_for_given_attributes?
|
68
68
|
errors.clear
|
69
69
|
|
70
|
-
ran
|
70
|
+
ran = []
|
71
|
+
keys = []
|
71
72
|
|
72
73
|
_validators.each do |attr, validators|
|
73
74
|
validators.each do |v|
|
74
75
|
next if !key?(attr) && v.skippable?
|
76
|
+
|
77
|
+
keys |= [attr.to_s]
|
78
|
+
|
75
79
|
next if ran.include?(v)
|
76
80
|
|
77
81
|
v.validate(self)
|
@@ -79,6 +83,10 @@ module Strobe
|
|
79
83
|
end
|
80
84
|
end
|
81
85
|
|
86
|
+
errors.keys.each do |k|
|
87
|
+
errors.delete(k) unless keys.include?(k.to_s)
|
88
|
+
end
|
89
|
+
|
82
90
|
errors.empty?
|
83
91
|
end
|
84
92
|
|
data/lib/strobe.rb
CHANGED
@@ -12,6 +12,8 @@ module Strobe
|
|
12
12
|
autoload :CLI, 'strobe/cli'
|
13
13
|
autoload :Collection, 'strobe/collection'
|
14
14
|
autoload :Connection, 'strobe/connection'
|
15
|
+
autoload :IdentityMap, 'strobe/identity_map'
|
16
|
+
autoload :Key, 'strobe/key'
|
15
17
|
autoload :Validations, 'strobe/validations'
|
16
18
|
|
17
19
|
module Resource
|
@@ -24,7 +26,9 @@ module Strobe
|
|
24
26
|
module Resources
|
25
27
|
autoload :Account, 'strobe/resources/account'
|
26
28
|
autoload :Application, 'strobe/resources/application'
|
29
|
+
autoload :Assignment, 'strobe/resources/assignment'
|
27
30
|
autoload :Me, 'strobe/resources/me'
|
31
|
+
autoload :Membership, 'strobe/resources/membership'
|
28
32
|
autoload :Signup, 'strobe/resources/signup'
|
29
33
|
autoload :Team, 'strobe/resources/team'
|
30
34
|
autoload :User, 'strobe/resources/user'
|
@@ -34,6 +38,7 @@ module Strobe
|
|
34
38
|
class StrobeError < StandardError ; end
|
35
39
|
class UnauthenticatedError < StrobeError ; end
|
36
40
|
class ResourceNotFoundError < StrobeError ; end
|
41
|
+
class ServerError < StrobeError ; end
|
37
42
|
|
38
43
|
def self.connection
|
39
44
|
@connection
|
metadata
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: strobe
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
prerelease:
|
4
|
+
prerelease: true
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
+
- 1
|
7
8
|
- 0
|
8
|
-
-
|
9
|
+
- beta
|
9
10
|
- 1
|
10
|
-
version: 0.0.
|
11
|
+
version: 0.1.0.beta.1
|
11
12
|
platform: ruby
|
12
13
|
authors:
|
13
14
|
- Yehuda Katz
|
@@ -16,7 +17,7 @@ autorequire:
|
|
16
17
|
bindir: bin
|
17
18
|
cert_chain: []
|
18
19
|
|
19
|
-
date:
|
20
|
+
date: 2011-01-02 00:00:00 -08:00
|
20
21
|
default_executable:
|
21
22
|
dependencies:
|
22
23
|
- !ruby/object:Gem::Dependency
|
@@ -106,18 +107,23 @@ extra_rdoc_files: []
|
|
106
107
|
|
107
108
|
files:
|
108
109
|
- lib/strobe/association.rb
|
109
|
-
- lib/strobe/cli/
|
110
|
-
- lib/strobe/cli/actions.rb
|
110
|
+
- lib/strobe/cli/main.rb
|
111
111
|
- lib/strobe/cli/settings.rb
|
112
|
+
- lib/strobe/cli/table.rb
|
113
|
+
- lib/strobe/cli/users.rb
|
112
114
|
- lib/strobe/cli.rb
|
113
115
|
- lib/strobe/collection.rb
|
114
116
|
- lib/strobe/connection.rb
|
117
|
+
- lib/strobe/identity_map.rb
|
118
|
+
- lib/strobe/key.rb
|
115
119
|
- lib/strobe/resource/base.rb
|
116
120
|
- lib/strobe/resource/collection.rb
|
117
121
|
- lib/strobe/resource/singleton.rb
|
118
122
|
- lib/strobe/resources/account.rb
|
119
123
|
- lib/strobe/resources/application.rb
|
124
|
+
- lib/strobe/resources/assignment.rb
|
120
125
|
- lib/strobe/resources/me.rb
|
126
|
+
- lib/strobe/resources/membership.rb
|
121
127
|
- lib/strobe/resources/signup.rb
|
122
128
|
- lib/strobe/resources/team.rb
|
123
129
|
- lib/strobe/resources/user.rb
|
data/lib/strobe/cli/action.rb
DELETED
@@ -1,131 +0,0 @@
|
|
1
|
-
require 'highline'
|
2
|
-
|
3
|
-
module Strobe
|
4
|
-
class CLI::Action
|
5
|
-
def self.run(action, settings, options = {})
|
6
|
-
success = false
|
7
|
-
klass = action.to_s.classify + 'Action'
|
8
|
-
klass = CLI::Actions.const_get(klass)
|
9
|
-
|
10
|
-
until success == true
|
11
|
-
action = klass.new(settings, options)
|
12
|
-
action.steps
|
13
|
-
success = !action.restart
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
include Resources
|
18
|
-
|
19
|
-
attr_reader :actions, :settings, :restart, :options
|
20
|
-
|
21
|
-
def initialize(settings, options)
|
22
|
-
@settings = settings
|
23
|
-
@options = options
|
24
|
-
@highline = HighLine.new
|
25
|
-
@resource = nil
|
26
|
-
|
27
|
-
# State
|
28
|
-
@in_group = false
|
29
|
-
@restart = false
|
30
|
-
end
|
31
|
-
|
32
|
-
def resource(resource = nil)
|
33
|
-
@resource = resource if resource
|
34
|
-
@resource
|
35
|
-
end
|
36
|
-
|
37
|
-
def say(what)
|
38
|
-
puts what
|
39
|
-
end
|
40
|
-
|
41
|
-
def agree(msg, *args)
|
42
|
-
@highline.agree(msg, *args) do |q|
|
43
|
-
next unless STDIN.tty? # For testing
|
44
|
-
q.readline = true
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def ask(key, options = {})
|
49
|
-
input key, options do |msg|
|
50
|
-
resp = @highline.ask msg do |q|
|
51
|
-
next unless STDIN.tty? # For testing
|
52
|
-
q.readline = true
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
def password(key, options = {})
|
58
|
-
input key, options do |msg|
|
59
|
-
@highline.ask msg do |q|
|
60
|
-
next unless STDIN.tty? # For testing
|
61
|
-
q.echo = '*'
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
def choose
|
67
|
-
@highline.choose do |menu|
|
68
|
-
yield menu
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
def group(options = {}, &blk)
|
73
|
-
case
|
74
|
-
when key = options[:validate]
|
75
|
-
validation_group(key, &blk)
|
76
|
-
else
|
77
|
-
yield if block_given?
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
def save(opts = {})
|
82
|
-
if resource.save
|
83
|
-
yield
|
84
|
-
else
|
85
|
-
display_errors
|
86
|
-
if opts[:on_error] == :restart
|
87
|
-
@restart = true
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
def run(action, opts = {})
|
93
|
-
CLI::Action.run(action, settings, opts)
|
94
|
-
end
|
95
|
-
|
96
|
-
private
|
97
|
-
|
98
|
-
def input(key, options)
|
99
|
-
into = options[:into] || key
|
100
|
-
|
101
|
-
validation_group into do
|
102
|
-
msg = (key.to_s.humanize + ': ').ljust(25)
|
103
|
-
resource[into] = yield msg
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
def validation_group(resource_key)
|
108
|
-
return yield if @in_group
|
109
|
-
|
110
|
-
begin
|
111
|
-
@in_group = true
|
112
|
-
while true
|
113
|
-
yield
|
114
|
-
|
115
|
-
break if resource.valid_attribute?(resource_key)
|
116
|
-
|
117
|
-
display_errors
|
118
|
-
end
|
119
|
-
ensure
|
120
|
-
@in_group = false
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
def display_errors
|
125
|
-
resource.errors.each do |k, msg|
|
126
|
-
key = k.to_s.split('.').last
|
127
|
-
say "[ERROR] #{key.humanize} #{msg}"
|
128
|
-
end
|
129
|
-
end
|
130
|
-
end
|
131
|
-
end
|
data/lib/strobe/cli/actions.rb
DELETED
@@ -1,104 +0,0 @@
|
|
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
|
-
config = options[:config]
|
55
|
-
path = determine_path
|
56
|
-
|
57
|
-
if application_id = config[:application_id]
|
58
|
-
resource Application.get application_id, :lazy => true
|
59
|
-
else
|
60
|
-
resource Account.first.applications.new
|
61
|
-
end
|
62
|
-
|
63
|
-
resource[:path] = path
|
64
|
-
|
65
|
-
unless resource.persisted?
|
66
|
-
say "Please fill out the following information."
|
67
|
-
|
68
|
-
ask :name
|
69
|
-
ask :url
|
70
|
-
|
71
|
-
save do
|
72
|
-
config[:application_id] ||= resource[:id]
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
resource.deploy!
|
77
|
-
say "The application has successfully been deployed and is available at #{resource[:url]}"
|
78
|
-
end
|
79
|
-
|
80
|
-
private
|
81
|
-
|
82
|
-
def determine_path
|
83
|
-
path = options[:path]
|
84
|
-
|
85
|
-
if File.exist?("#{path}/tmp/build")
|
86
|
-
return "#{path}/tmp/build"
|
87
|
-
end
|
88
|
-
|
89
|
-
path
|
90
|
-
end
|
91
|
-
|
92
|
-
def ensure_computer_is_registered
|
93
|
-
unless settings[:token]
|
94
|
-
say "This computer is not yet registered with a Strobe account."
|
95
|
-
|
96
|
-
choose do |m|
|
97
|
-
m.choice(:login) { run :login }
|
98
|
-
m.choice(:signup) { run :signup }
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|
104
|
-
end
|