couch-shell 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
data/lib/couch-shell.rb CHANGED
@@ -42,6 +42,11 @@ module CouchShell
42
42
  end
43
43
 
44
44
  shell = Shell.new(STDIN, STDOUT, STDERR)
45
+ shell.plugin "core"
46
+ shell.plugin "core_edit"
47
+ shell.plugin "core_views"
48
+ shell.plugin "core_designs"
49
+ shell.plugin "core_lucene"
45
50
  shell.execute "user #{user}" if user
46
51
  shell.execute "server #{server}" if server
47
52
  shell.execute "cg #{path}" if path
@@ -1,18 +1,41 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
 
3
+ require "couch-shell/exceptions"
4
+
3
5
  module CouchShell
4
6
 
5
7
  class EvalContext < BasicObject
6
8
 
9
+ # weird trickery ahead
10
+ # we want only instance_variable_set from kernel
11
+ include ::Kernel
12
+ alias_method :_instance_variable_set, :instance_variable_set
13
+ (::Kernel.instance_methods + ::Kernel.private_instance_methods).each { |m|
14
+ # Ruby 1.9.2p0 prints a warning when undefining object_id,
15
+ # although BasicObject doesn't define it anyway, making this
16
+ # warning obsolete IMHO.
17
+ if m == :object_id
18
+ oldv = $VERBOSE
19
+ begin
20
+ $VERBOSE = nil
21
+ undef_method m
22
+ ensure
23
+ $VERBOSE = oldv
24
+ end
25
+ else
26
+ undef_method m
27
+ end
28
+ }
29
+
7
30
  def initialize(vardict)
8
- @vardict = vardict
31
+ @_vardict = vardict
9
32
  end
10
33
 
11
34
  def method_missing(msg, *args)
12
35
  if args.empty?
13
- @vardict.lookup_var msg.to_s
36
+ @_vardict.lookup_var msg.to_s
14
37
  else
15
- super
38
+ raise ShellUserError, "unexpected syntax at #{msg}"
16
39
  end
17
40
  end
18
41
 
@@ -0,0 +1,65 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module CouchShell
4
+
5
+ class Quit < Exception
6
+ end
7
+
8
+ class ShellUserError < Exception
9
+ end
10
+
11
+ class UndefinedVariable < ShellUserError
12
+
13
+ attr_reader :varname
14
+
15
+ def initialize(varname)
16
+ @varname = varname
17
+ end
18
+
19
+ end
20
+
21
+ class NoSuchPluginRegistered < ShellUserError
22
+
23
+ attr_reader :plugin_name
24
+
25
+ def initialize(plugin_name)
26
+ @plugin_name = plugin_name
27
+ end
28
+
29
+ def message
30
+ "No such plugin registered: #@plugin_name"
31
+ end
32
+
33
+ end
34
+
35
+ class NoSuchCommandInPlugin < ShellUserError
36
+
37
+ attr_reader :plugin_name
38
+ attr_reader :command_name
39
+
40
+ def initialize(plugin_name, command_name)
41
+ @plugin_name = plugin_name
42
+ @command_name = command_name
43
+ end
44
+
45
+ def message
46
+ "Plugin #@plugin_name doesn't define a #@command_name command."
47
+ end
48
+
49
+ end
50
+
51
+ class NoSuchCommand < ShellUserError
52
+
53
+ attr_reader :command_name
54
+
55
+ def initialize(command_name)
56
+ @command_name = command_name
57
+ end
58
+
59
+ def message
60
+ "No such command: #@command_name"
61
+ end
62
+
63
+ end
64
+
65
+ end
@@ -4,12 +4,31 @@ require "json"
4
4
 
5
5
  module CouchShell
6
6
 
7
+ # Wraps a Ruby data structure that represents a json value. If the wrapped
8
+ # document is of type object, members can be accessed method call syntax. To
9
+ # avoid shadowing of object members, almost all JsonValue instance methods
10
+ # end in ! or ?.
11
+ #
12
+ # j = JsonValue.wrap({"a" => 1})
13
+ # j.a # => #<JsonValue 1>
14
+ # j.unwrapped! # => {"a" => 1}
15
+ #
16
+ # Attributes can also be accessed via []
17
+ #
18
+ # j["a"] # => #<JsonValue 1>
19
+ #
20
+ # Arrays elements too
21
+ #
22
+ # j = JsonValue.wrap(["a", "b"])
23
+ # j[0] # => #<JsonValue "a">
24
+ #
25
+ # The wrapped data structure mustn't be modified.
7
26
  class JsonValue < BasicObject
8
27
 
9
28
  def self.wrap(ruby_value)
10
29
  case ruby_value
11
30
  when ::Hash
12
- h = ::Hash.new(ruby_value.size)
31
+ h = ::Hash.new
13
32
  ruby_value.each { |k, v|
14
33
  h[k] = wrap(v)
15
34
  }
@@ -36,6 +55,14 @@ module CouchShell
36
55
  end
37
56
  end
38
57
 
58
+ def self.rval(obj)
59
+ if obj.respond_to?(:couch_shell_ruby_value!)
60
+ obj.couch_shell_ruby_value!
61
+ else
62
+ obj
63
+ end
64
+ end
65
+
39
66
  def initialize(value, ruby_value)
40
67
  @value = value
41
68
  @ruby_value = ruby_value
@@ -59,9 +86,32 @@ module CouchShell
59
86
  end
60
87
  end
61
88
 
89
+ def object?
90
+ @type == :object
91
+ end
92
+
93
+ def array?
94
+ @type == :array
95
+ end
96
+
97
+ def string?
98
+ @type == :string
99
+ end
100
+
101
+ def number?
102
+ @type == :number
103
+ end
104
+
105
+ def boolean?
106
+ @type == :boolean
107
+ end
108
+
109
+ def null?
110
+ @type == :null
111
+ end
112
+
62
113
  def respond_to?(msg)
63
- msg == :format || msg == :couch_shell_format_string ||
64
- msg == :type || msg == :ruby_value || msg == :to_s ||
114
+ msg == :couch_shell_format_string! || msg == :to_s ||
65
115
  msg == :couch_shell_ruby_value! ||
66
116
  (@type == :object && @value.has_key?(msg.to_s))
67
117
  end
@@ -76,35 +126,47 @@ module CouchShell
76
126
  super
77
127
  end
78
128
 
129
+ # Access object member (i must be a string) or array element (i must be an
130
+ # integer). Returns a JsonValue or nil if the object member or array index
131
+ # doesn't exist.
79
132
  def [](i)
80
- ::Kernel.raise ::TypeError unless @type == :array
81
- @value[i]
82
- end
83
-
84
- def to_s
85
- case @type
86
- when :object, :array
87
- ::JSON.generate(@ruby_value)
88
- when :null
89
- "null"
133
+ ri = JsonValue.rval(i)
134
+ case ri
135
+ when ::String
136
+ unless @type == :object
137
+ ::Kernel.raise ::TypeError,
138
+ "string indexing only allowed for objects"
139
+ end
140
+ @value[ri]
141
+ when ::Integer
142
+ unless @type == :array
143
+ ::Kernel.raise ::TypeError,
144
+ "integer indexing only allowed for arrays"
145
+ end
146
+ @value[ri]
90
147
  else
91
- @ruby_value.to_s
148
+ ::Kernel.raise ::TypeError,
149
+ "index must be string or integer"
92
150
  end
93
151
  end
94
152
 
95
- def format
153
+ def to_s(format = false)
96
154
  case @type
97
155
  when :object, :array
98
- ::JSON.pretty_generate(@ruby_value)
156
+ if format
157
+ ::JSON.pretty_generate(@ruby_value)
158
+ else
159
+ ::JSON.generate(@ruby_value)
160
+ end
99
161
  when :null
100
162
  "null"
101
- when
163
+ else
102
164
  @ruby_value.to_s
103
165
  end
104
166
  end
105
167
 
106
- def couch_shell_format_string
107
- format
168
+ def couch_shell_format_string!
169
+ to_s true
108
170
  end
109
171
 
110
172
  def delete_attr!(name)
@@ -124,6 +186,7 @@ module CouchShell
124
186
  def couch_shell_ruby_value!
125
187
  @ruby_value
126
188
  end
189
+ alias unwrapped! couch_shell_ruby_value!
127
190
 
128
191
  def nil?
129
192
  false
@@ -134,6 +197,15 @@ module CouchShell
134
197
  @value[name]
135
198
  end
136
199
 
200
+ def inspect
201
+ "#<JsonValue #{to_s}>"
202
+ end
203
+
204
+ def length
205
+ raise ::TypeError, "length of #@type" unless array?
206
+ @ruby_value.length
207
+ end
208
+
137
209
  end
138
210
 
139
211
  end
@@ -0,0 +1,251 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require "unicode_utils/uppercase_char_q"
4
+ require "unicode_utils/downcase"
5
+ require "decorate"
6
+ require "couch-shell/exceptions"
7
+ require "couch-shell/plugin_utils"
8
+
9
+ module CouchShell
10
+
11
+ class VariableInfo
12
+
13
+ attr_reader :name
14
+ attr_reader :prefix
15
+ attr_reader :tags
16
+ attr_reader :doc_line
17
+ attr_reader :doc_text
18
+ attr_reader :lookup_message
19
+ attr_reader :plugin
20
+
21
+ def initialize(opts)
22
+ @name = opts[:name]
23
+ @prefix = opts[:prefix]
24
+ raise "name or prefix required" unless @name || @prefix
25
+ raise "only name OR prefix allowed" if @name && @prefix
26
+ @tags = [@name || @prefix].concat(opts[:tags] || [])
27
+ @doc_line = opts[:doc_line] or raise "doc_line required"
28
+ @doc_text = opts[:doc_text]
29
+ @lookup_message = opts[:lookup_message] or raise "lookup_message required"
30
+ @plugin = opts[:plugin] or raise "plugin required"
31
+ end
32
+
33
+ def label
34
+ @name || "#@prefix*"
35
+ end
36
+
37
+ end
38
+
39
+ class CommandInfo
40
+
41
+ attr_reader :name
42
+ attr_reader :tags
43
+ attr_reader :synopsis
44
+ attr_reader :doc_line
45
+ attr_reader :doc_text
46
+ attr_reader :execute_message
47
+ attr_reader :plugin
48
+
49
+ def initialize(opts)
50
+ @name = opts[:name] or raise "name required"
51
+ @tags = [@name].concat(opts[:tags] || [])
52
+ @synopsis = opts[:synopsis]
53
+ @doc_line = opts[:doc_line] or raise "doc_line required"
54
+ @doc_text = opts[:doc_text]
55
+ @execute_message = opts[:execute_message] or raise "execute_message required"
56
+ @plugin = opts[:plugin] or raise "plugin required"
57
+ end
58
+
59
+ end
60
+
61
+ class PluginInfo
62
+
63
+ @by_class_map = {}
64
+ @by_name_map = {}
65
+
66
+ class << self
67
+
68
+ def class_name_to_plugin_name(class_name)
69
+ String.new.tap do |plugin_name|
70
+ was_uppercase = false
71
+ extract = class_name[/[^:]+\z/].sub(/Plugin\z/, '')
72
+ i = 0
73
+ lastcase = nil
74
+ while i < extract.length
75
+ c = extract[i]
76
+ if UnicodeUtils.uppercase_char? c
77
+ if lastcase != :upper
78
+ if i > 0 && plugin_name[-1] != "_"
79
+ plugin_name << "_"
80
+ end
81
+ lastcase = :upper
82
+ end
83
+ plugin_name << UnicodeUtils.downcase(c, nil)
84
+ elsif UnicodeUtils.lowercase_char? c
85
+ if lastcase == :upper &&
86
+ (plugin_name.length > 1 && plugin_name[-2] != "_")
87
+ plugin_name.insert(-2, "_")
88
+ end
89
+ lastcase = :lower
90
+ plugin_name << c
91
+ else
92
+ lastcase = nil
93
+ plugin_name << c
94
+ end
95
+ i = i + 1
96
+ end
97
+ end
98
+ end
99
+
100
+ def register(plugin_class)
101
+ plugin_name = class_name_to_plugin_name(plugin_class.name)
102
+ unless plugin_name =~ /\A\p{Alpha}(\p{Alpha}|\p{Digit}|_)*\z/
103
+ raise "invalid plugin name #{plugin_name}"
104
+ end
105
+ pi = PluginInfo.new(plugin_name, plugin_class)
106
+ @by_class_map[plugin_class] = pi
107
+ @by_name_map[plugin_name] = pi
108
+ end
109
+
110
+ def [](class_or_name)
111
+ @by_class_map[class_or_name] || @by_name_map[class_or_name]
112
+ end
113
+
114
+ end
115
+
116
+ attr_reader :plugin_class
117
+ attr_reader :plugin_name
118
+ # Enumerable of VariableInfo instances.
119
+ attr_reader :variables
120
+ # Map of name/CommandInfo instances.
121
+ attr_reader :commands
122
+
123
+ def initialize(plugin_name, plugin_class)
124
+ @plugin_class = plugin_class
125
+ @plugin_name = plugin_name
126
+ @variables = []
127
+ @commands = {}
128
+ end
129
+
130
+ def register_command(ci)
131
+ raise "command #{ci.name} already registered" if @commands[ci]
132
+ @commands[ci.name] = ci
133
+ end
134
+
135
+ def register_variable(variable_info)
136
+ @variables << variable_info
137
+ end
138
+
139
+ end
140
+
141
+ module PluginClass
142
+
143
+ def plugin_info
144
+ PluginInfo[self]
145
+ end
146
+
147
+ def var(doc_line, opts = {})
148
+ opts[:doc_line] = doc_line
149
+ Decorate.decorate { |klass, method_name|
150
+ unless opts[:name] || opts[:prefix]
151
+ if method_name =~ /\Alookup_prefix_(.+)\z/
152
+ opts[:prefix] = $1
153
+ elsif method_name =~ /\Alookup_(.+)\z/
154
+ opts[:name] = $1
155
+ end
156
+ end
157
+ opts[:lookup_message] = method_name
158
+ opts[:plugin] = plugin_info
159
+ plugin_info.register_variable VariableInfo.new(opts)
160
+ }
161
+ end
162
+
163
+ def cmd(doc_line, opts = {})
164
+ opts[:doc_line] = doc_line
165
+ Decorate.decorate { |klass, method_name|
166
+ if !opts[:name] && method_name =~ /\Aexecute_(.+)\z/
167
+ opts[:name] = $1
168
+ end
169
+ opts[:execute_message] = method_name
170
+ opts[:plugin] = plugin_info
171
+ plugin_info.register_command CommandInfo.new(opts)
172
+ }
173
+ end
174
+
175
+ end
176
+
177
+ class PluginVariablesObject < BasicObject
178
+
179
+ def initialize(plugin)
180
+ @plugin = plugin
181
+ end
182
+
183
+ def method_missing(msg, *args)
184
+ unless args.empty?
185
+ ::Kernel.raise ShellUserError,
186
+ "expected plugin variable lookup: #{msg}"
187
+ end
188
+ varname = msg.to_s
189
+ @plugin.plugin_info.variables.each { |vi|
190
+ begin
191
+ if vi.name && vi.name == varname
192
+ return @plugin.send vi.lookup_message
193
+ end
194
+ if vi.prefix && varname.start_with?(vi.prefix) &&
195
+ varname.length > vi.prefix.length
196
+ return @plugin.send vi.lookup_message, varname[vi.prefix.length]
197
+ end
198
+ rescue Plugin::VarNotSet => e
199
+ e.var = vi
200
+ ::Kernel.raise e
201
+ end
202
+ }
203
+ ::Kernel.raise ShellUserError,
204
+ "no variable #{varname} in plugin #{@plugin.plugin_name}"
205
+ end
206
+
207
+ end
208
+
209
+ class Plugin
210
+
211
+ include PluginUtils
212
+
213
+ def self.inherited(klass)
214
+ klass.extend PluginClass
215
+ PluginInfo.register(klass)
216
+ end
217
+
218
+ # Do not override this method, override plugin_initialization if you
219
+ # need custom initialization logic.
220
+ def initialize(shell)
221
+ @_couch_shell_shell = shell
222
+ end
223
+
224
+ def shell
225
+ @_couch_shell_shell
226
+ end
227
+
228
+ def plugin_info
229
+ self.class.plugin_info
230
+ end
231
+
232
+ def plugin_name
233
+ plugin_info.plugin_name
234
+ end
235
+
236
+ # Called by the shell after it instantiates the plugin. The shell
237
+ # attribute will be already set.
238
+ #
239
+ # Override this method if your plugin needs custom initialization
240
+ # logic or to alter the shell behaviour beyond adding variables and
241
+ # commands. The default implementation does nothing.
242
+ def plugin_initialization
243
+ end
244
+
245
+ def variables_object
246
+ PluginVariablesObject.new(self)
247
+ end
248
+
249
+ end
250
+
251
+ end