strelka 0.11.0 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,29 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'strelka/cli' unless defined?( Strelka::CLI )
5
+
6
+
7
+ # Command to show discovered Strelka apps
8
+ module Strelka::CLI::Discover
9
+ extend Strelka::CLI::Subcommand
10
+
11
+ desc 'Show installed Strelka apps'
12
+ command :discover do |cmd|
13
+
14
+ cmd.action do |globals, options, args|
15
+ prompt.say( headline_string "Searching for Strelka applications..." )
16
+
17
+ apps = Strelka::Discovery.discovered_apps
18
+
19
+ if apps.empty?
20
+ prompt.say "None found."
21
+ else
22
+ rows = apps.sort_by {|name, path| name }
23
+ display_table( rows )
24
+ end
25
+ end
26
+ end
27
+
28
+ end # module Strelka::CLI::Discover
29
+
@@ -0,0 +1,40 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'strelka/cli' unless defined?( Strelka::CLI )
5
+
6
+
7
+ # Command to start a Strelka application
8
+ module Strelka::CLI::Start
9
+ extend Strelka::CLI::Subcommand
10
+
11
+ desc 'Start a Strelka app'
12
+ arg :GEMNAME, :optional
13
+ arg :APPNAME
14
+ command :start do |cmd|
15
+
16
+ cmd.action do |globals, options, args|
17
+ appname = args.pop
18
+ gemname = args.pop
19
+ path = nil
20
+
21
+ gem( gemname ) if gemname
22
+
23
+ app = if File.exist?( appname )
24
+ Strelka::Discovery.load_file( appname ) or
25
+ exit_now!( "Didn't find an app while loading %p!" % [appname] )
26
+ else
27
+ Strelka::Discovery.load( appname ) or
28
+ exit_now!( "Couldn't find the %p app!" % [appname] )
29
+ end
30
+
31
+ Strelka::CLI.prompt.say "Starting %s (%p)." % [ appname, app ]
32
+ Strelka.load_config( options[:c] ) if options[:c]
33
+ unless_dryrun( "starting the app" ) do
34
+ app.run
35
+ end
36
+ end
37
+ end
38
+
39
+ end # module Strelka::CLI::Start
40
+
@@ -7,6 +7,83 @@ require 'strelka' unless defined?( Strelka )
7
7
 
8
8
 
9
9
  # The Strelka application-discovery system.
10
+ #
11
+ # This module provides a mechanism for registering Strelka apps and their
12
+ # resources for discovery by the strelka CLI and other systems.
13
+ #
14
+ # It's responsible for three kinds of discovery:
15
+ #
16
+ # - Discovery of Strelka app files via Rubygems discovery
17
+ # - Discovery and loading of Strelka app classes by name
18
+ # - Discovery of data directories for strelka apps
19
+ #
20
+ # As such it can be used in several different ways.
21
+ #
22
+ # == \App File \Discovery
23
+ #
24
+ # If you have an app that you wish to be discoverable, create a
25
+ # <tt>lib/strelka/apps.rb</tt> file. This file will be added to those returned
26
+ # by the ::app_discovery_files call, which is the list loaded by
27
+ # ::discovered_apps.
28
+ #
29
+ # == \App \Discovery Registration
30
+ #
31
+ # To add a name and file path to Strelka::Discovery.discovered_apps, you can
32
+ # call ::register_app. This will check to make sure no other apps are registered
33
+ # with the same name. To register several at the same time, call
34
+ # ::register_apps with a Hash of <tt>name => path</tt> pairs.
35
+ #
36
+ # == Loading Discovered Apps
37
+ #
38
+ # To load a discovered app, call ::load with its registered name.
39
+ #
40
+ # This will load the associated file and returns the first Ruby class to inherit
41
+ # from a discoverable app class like Strelka::App or Strelka::WebSocketServer.
42
+ #
43
+ # === Putting it all together
44
+ #
45
+ # Say, for example, you were putting together an <tt>acme-apps</tt> gem for the
46
+ # Acme company that contained Strelka apps for a web store and a CMS. You could
47
+ # add a <tt>lib/strelka/apps.rb</tt> file to the <tt>acme-apps</tt> gem that
48
+ # contained the following:
49
+ #
50
+ # # -*- ruby -*-
51
+ # require 'strelka/discovery'
52
+ #
53
+ # Strelka::Discovery.register_apps(
54
+ # 'acme-store' => 'lib/acme/store.rb',
55
+ # 'acme-cms' => 'lib/acme/cms.rb'
56
+ # )
57
+ #
58
+ # This would let you do:
59
+ #
60
+ # $ gem install acme-apps
61
+ # $ strelka start acme-store
62
+ #
63
+ #
64
+ # == Data Directory \Discovery
65
+ #
66
+ # If your app requires some filesystem resources, a good way to distribute these
67
+ # is in your gem's "data directory". This is a directory in your gem called
68
+ # <tt>data/«your gem name»</tt>, and can be found via:
69
+ #
70
+ # Gem.datadir( your_gem_name )
71
+ #
72
+ # Strelka::Discoverable builds on top of this, and can return a Hash of glob
73
+ # patterns that will match the data directories of all gems that depend on
74
+ # Strelka, keyed by gem name. You can use this to populate search paths for
75
+ # templates, static assets, etc.
76
+ #
77
+ # template_paths = Strelka::Discovery.discover_data_dirs.
78
+ # flat_map {|_, pattern| Dir.glob(pattern + '/templates') }
79
+ #
80
+ # == Making a class Discoverable
81
+ #
82
+ # If you write your own app base class (e.g., Strelka::App,
83
+ # Strelka::WebSocketServer), you can make it discoverable by extending it with
84
+ # this module. You typically won't have to do this unless you're working on
85
+ # Strelka itself.
86
+ #
10
87
  module Strelka::Discovery
11
88
  extend Loggability,
12
89
  Configurability,
@@ -22,7 +99,7 @@ module Strelka::Discovery
22
99
 
23
100
  # Default config
24
101
  CONFIG_DEFAULTS = {
25
- app_glob_pattern: '{apps,handlers}/**/*',
102
+ app_discovery_file: 'strelka/apps.rb',
26
103
  local_data_dirs: 'data/*',
27
104
  }.freeze
28
105
 
@@ -30,11 +107,11 @@ module Strelka::Discovery
30
107
  ##
31
108
  # The Hash of Strelka::App subclasses, keyed by the Pathname of the file they were
32
109
  # loaded from, or +nil+ if they weren't loaded via ::load.
33
- singleton_attr_reader :subclasses
110
+ singleton_attr_reader :discovered_classes
34
111
 
35
112
  ##
36
- # The glob(3) pattern for matching Apps during discovery
37
- singleton_attr_accessor :app_glob_pattern
113
+ # The glob(3) pattern for matching the discovery hook file.
114
+ singleton_attr_accessor :app_discovery_file
38
115
 
39
116
  ##
40
117
  # The glob(3) pattern for matching local data directories during discovery. Local
@@ -46,11 +123,55 @@ module Strelka::Discovery
46
123
  singleton_attr_reader :loading_file
47
124
 
48
125
 
49
- # Module instance variables
50
- @subclasses = Hash.new {|h,k| h[k] = [] }
51
- @loading_file = nil
52
- @app_glob_pattern = CONFIG_DEFAULTS[:app_glob_pattern]
126
+ # Class instance variables
127
+ @discovered_classes = Hash.new {|h,k| h[k] = [] }
128
+ @app_discovery_file = CONFIG_DEFAULTS[:app_discovery_file]
53
129
  @local_data_dirs = CONFIG_DEFAULTS[:local_data_dirs]
130
+ @discovered_apps = nil
131
+
132
+
133
+ ### Register an app with the specified +name+ that can be loaded from the given
134
+ ### +path+.
135
+ def self::register_app( name, path )
136
+ @discovered_apps ||= {}
137
+
138
+ if @discovered_apps.key?( name )
139
+ raise "Can't register a second '%s' app at %s; already have one at %s" %
140
+ [ name, path, @discovered_apps[name] ]
141
+ end
142
+
143
+ self.log.debug "Registered app at %s as %p" % [ path, name ]
144
+ @discovered_apps[ name ] = path
145
+ end
146
+
147
+
148
+ ### Register multiple apps by passing +a_hash+ of names and paths.
149
+ def self::register_apps( a_hash )
150
+ a_hash.each do |name, path|
151
+ self.register_app( name, path )
152
+ end
153
+ end
154
+
155
+
156
+ ### Return a Hash of apps discovered by loading #app_discovery_files.
157
+ def self::discovered_apps
158
+ unless @discovered_apps
159
+ @discovered_apps ||= {}
160
+ self.app_discovery_files.each do |path|
161
+ self.log.debug "Loading discovery file %p" % [ path ]
162
+ Kernel.load( path )
163
+ end
164
+ end
165
+
166
+ return @discovered_apps
167
+ end
168
+
169
+
170
+ ### Return an Array of app discovery hook files found in the latest installed gems and
171
+ ### the current $LOAD_PATH.
172
+ def self::app_discovery_files
173
+ return Gem.find_latest_files( self.app_discovery_file )
174
+ end
54
175
 
55
176
 
56
177
  ### Configure the App. Override this if you wish to add additional configuration
@@ -59,7 +180,7 @@ module Strelka::Discovery
59
180
  def self::configure( config=nil )
60
181
  config = self.defaults.merge( config || {} )
61
182
 
62
- self.app_glob_pattern = config[:app_glob_pattern]
183
+ self.app_discovery_file = config[:app_discovery_file]
63
184
  self.local_data_dirs = config[:local_data_dirs]
64
185
  end
65
186
 
@@ -92,103 +213,36 @@ module Strelka::Discovery
92
213
  end
93
214
 
94
215
 
95
- ### Return a Hash of Strelka app files as Pathname objects from installed gems,
96
- ### keyed by gemspec name .
97
- def self::discover_paths
98
- appfiles = {}
216
+ ### Attempt to load the file associated with the specified +app_name+ and return
217
+ ### the first Strelka::App class declared in the process.
218
+ def self::load( app_name )
219
+ apps = self.discovered_apps or return nil
220
+ file = apps[ app_name ] or return nil
99
221
 
100
- self.discover_data_dirs.each do |gemname, dir|
101
- pattern = File.join( dir, self.app_glob_pattern )
102
- appfiles[ gemname ] = Pathname.glob( pattern )
103
- end
104
-
105
- return appfiles
222
+ return self.load_file( file )
106
223
  end
107
224
 
108
225
 
109
- ### Return an Array of Strelka::App classes loaded from the installed Strelka gems.
110
- def self::discover
111
- discovered_apps = []
112
- app_paths = self.discover_paths
113
-
114
- self.log.debug "Loading apps from %d discovered paths" % [ app_paths.length ]
115
- app_paths.each do |gemname, paths|
116
- self.log.debug " loading gem %s" % [ gemname ]
117
- gem( gemname ) unless gemname == ''
118
-
119
- self.log.debug " loading apps from %s: %d handlers" % [ gemname, paths.length ]
120
- paths.each do |path|
121
- classes = begin
122
- self.load( path )
123
- rescue StandardError, ScriptError => err
124
- self.log.error "%p while loading Strelka apps from %s: %s" %
125
- [ err.class, path, err.message ]
126
- self.log.debug "Backtrace: %s" % [ err.backtrace.join("\n\t") ]
127
- []
128
- end
129
- self.log.debug " loaded app classes: %p" % [ classes ]
130
-
131
- discovered_apps += classes
132
- end
133
- end
134
-
135
- return discovered_apps
136
- end
137
-
226
+ ### Load the specified +file+ and return the first class that extends Strelka::Discovery.
227
+ def self::load_file( file )
228
+ self.log.debug "Loading application/s from %p" % [ file ]
229
+ Thread.current[ :__loading_file ] = loading_file = file
230
+ self.discovered_classes.delete( loading_file )
138
231
 
139
- ### Find the first app with the given +appname+ and return the path to its file and the name of
140
- ### the gem it's from. If the optional +gemname+ is given, only consider apps from that gem.
141
- ### Raises a RuntimeError if no app with the given +appname+ was found.
142
- def self::find( appname, gemname=nil )
143
- discovered_apps = self.discover_paths
144
-
145
- path = nil
146
- if gemname
147
- discovered_apps[ gemname ].each do |apppath|
148
- self.log.debug " %s (%s)" % [ apppath, apppath.basename('.rb') ]
149
- if apppath.basename('.rb').to_s == appname
150
- path = apppath
151
- break
152
- end
153
- end
154
- else
155
- self.log.debug "No gem name; searching them all:"
156
- discovered_apps.each do |disc_gemname, paths|
157
- self.log.debug " %s: %d paths" % [ disc_gemname, paths.length ]
158
- path = paths.find do |apppath|
159
- self.log.debug " %s (%s)" % [ apppath, apppath.basename('.rb') ]
160
- self.log.debug " %p vs. %p" % [ apppath.basename('.rb').to_s, appname ]
161
- apppath.basename('.rb').to_s == appname
162
- end or next
163
- gemname = disc_gemname
164
- break
165
- end
166
- end
232
+ Kernel.load( loading_file.to_s )
167
233
 
168
- unless path
169
- msg = "Couldn't find an app named '#{appname}'"
170
- msg << " in the #{gemname} gem" if gemname
171
- raise( msg )
172
- end
173
- self.log.debug " found: %s" % [ path ]
234
+ new_subclasses = self.discovered_classes[ loading_file ]
235
+ self.log.debug " loaded %d new app class/es" % [ new_subclasses.size ]
174
236
 
175
- return path, gemname
237
+ return new_subclasses.first
238
+ ensure
239
+ Thread.current[ :__loading_file ] = nil
176
240
  end
177
241
 
178
242
 
179
- ### Load the specified +file+, and return any Strelka::App subclasses that are loaded
180
- ### as a result.
181
- def self::load( file )
182
- self.log.debug "Loading application/s from %p" % [ file ]
183
- @loading_file = Pathname( file ).expand_path
184
- self.subclasses.delete( @loading_file )
185
- Kernel.load( @loading_file.to_s )
186
- new_subclasses = self.subclasses[ @loading_file ]
187
- self.log.debug " loaded %d new app class/es" % [ new_subclasses.size ]
188
-
189
- return new_subclasses
190
- ensure
191
- @loading_file = nil
243
+ ### Return the Pathname of the file being loaded by the current thread (if there is one)
244
+ def self::loading_file
245
+ return Thread.current[ :__loading_file ]
192
246
  end
193
247
 
194
248
 
@@ -196,7 +250,7 @@ module Strelka::Discovery
196
250
  ### with Discovery.
197
251
  def self::add_inherited_class( subclass )
198
252
  self.log.debug "Registering discovered subclass %p" % [ subclass ]
199
- self.subclasses[ self.loading_file ] << subclass
253
+ self.discovered_classes[ self.loading_file ] << subclass
200
254
  end
201
255
 
202
256
 
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env rspec -cfd
2
+ #encoding: utf-8
3
+
4
+ require_relative '../helpers'
5
+
6
+ require 'tempfile'
7
+ require 'rspec'
8
+
9
+ require 'strelka/cli'
10
+
11
+ describe Strelka::CLI do
12
+
13
+ before( :all ) do
14
+ testcommands = Module.new
15
+ testcommands.extend( Strelka::CLI::Subcommand )
16
+ testcommands.module_eval do
17
+ command :test_output do |cmd|
18
+ cmd.action do
19
+ prompt.say "Test command!"
20
+ end
21
+ end
22
+
23
+ command :test_dryrun do |cmd|
24
+ cmd.action do
25
+ unless_dryrun( "Running the test." ) do
26
+ $stdout.puts "Ran it!"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ after( :each ) do
34
+ described_class.reset_prompt
35
+ end
36
+
37
+ describe "output redirection" do
38
+
39
+ it "uses STDERR for user interaction" do
40
+ expect {
41
+ described_class.run([ 'test_output' ])
42
+ }.to output( /Test command!\n/ ).to_stderr
43
+ end
44
+
45
+
46
+ it "redirects its output to STDOUT when run with `-o -`" do
47
+ expect {
48
+ described_class.run([ '-o', '-', 'test_output' ])
49
+ }.to output( /Test command!\n/ ).to_stdout
50
+ end
51
+
52
+
53
+ it "redirects its output to the named file when run with `-o filename`" do
54
+ tmpfile = Dir::Tmpname.create( 'strelka-command-fileout' ) { }
55
+
56
+ begin
57
+ described_class.run([ '-o', tmpfile, 'test_output' ])
58
+ expect( IO.read(tmpfile) ).to match( /Test command!\n/ )
59
+ ensure
60
+ File.unlink( tmpfile ) if tmpfile && File.exist?( tmpfile )
61
+ end
62
+ end
63
+
64
+ end
65
+
66
+
67
+ describe "dry-run mode" do
68
+
69
+ it "executes the protected block if dry-run mode isn't enabled" do
70
+ expect {
71
+ described_class.run([ 'test_dryrun' ])
72
+ }.to output( /Ran it!/ ).to_stdout
73
+ end
74
+
75
+
76
+ it "doesn't execute the block if dry-run mode *is* enabled" do
77
+ expect {
78
+ described_class.run([ '-n', 'test_dryrun' ])
79
+ }.to_not output( /Ran it!/ ).to_stdout
80
+ end
81
+
82
+ end
83
+
84
+ end
85
+