strelka 0.11.0 → 0.12.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.
@@ -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
+