com 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,225 @@
1
+ # COM #
2
+
3
+ COM is an object-oriented wrapper around WIN32OLE. COM makes it easy to add
4
+ behavior to WIN32OLE objects, making them easier to work with from Ruby.
5
+
6
+
7
+ ## Usage ##
8
+
9
+ Using COM is rather straightforward. There’s basically four concepts to keep
10
+ track of:
11
+
12
+ 1. COM objects
13
+ 2. Instantiable COM objects
14
+ 3. COM events
15
+ 4. COM errors
16
+
17
+ Let’s look at each concept separately, using the following example as a base.
18
+
19
+ module Word end
20
+
21
+ class Word::Application < COM::Instantiable
22
+ def without_interaction
23
+ with_properties('displayalerts' => Word::WdAlertsNone){ yield }
24
+ end
25
+
26
+ def documents
27
+ Word::Documents.new(com.documents)
28
+ end
29
+
30
+ def quit(saving = Word::WdDoNotSaveChanges, *args)
31
+ com.quit saving, *args
32
+ end
33
+ end
34
+
35
+ ### COM Objects ###
36
+
37
+ A COM::Object is a wrapper around a COM object. It provides error
38
+ specialization, which is discussed later and a few utility methods. You
39
+ typically use it to wrap COM objects that are returned by COM methods. If we
40
+ take the example given in the introduction, Word::Documents is a good
41
+ candidate:
42
+
43
+ class Word::Documents < COM::Object
44
+ DefaultOpenOptions = {
45
+ 'confirmconversions' => false,
46
+ 'readonly' => true,
47
+ 'addtorecentfiles' => false,
48
+ 'visible' => false
49
+ }.freeze
50
+ def open(path, options = {})
51
+ options = DefaultOpenOptions.merge(options)
52
+ options['filename'] = Pathname(path).to_com
53
+ Word::Document.new(com.open(options))
54
+ end
55
+ end
56
+
57
+ Here we override the #open method to be a bit easier to use, providing sane
58
+ defaults for COM interaction. Worth noting is the use of the #com method to
59
+ access the actual COM object to invoke the #open method on it. Also note that
60
+ Word::Document is also a COM::Object.
61
+
62
+ COM::Object provides a convenience method called #with_properties, which is
63
+ used in the #without_interaction method above. It lets you set properties on
64
+ the COM::Object during the duration of a block, restoring them after it exits
65
+ (successfully or with an error).
66
+
67
+
68
+ ### Instantiable COM Objects ###
69
+
70
+ Instantiable COM objects are COM objects that we can connect to and that can be
71
+ created. The Word::Application object can, for example, be created.
72
+ Instantiable COM objects should inherit from COM::Instantiable. Instantiable
73
+ COM objects can be told what program ID to use, whether or not to allow
74
+ connecting to an already running object, and to load its associated constants
75
+ upon creation.
76
+
77
+ The program ID is used to determine what instantiable COM object to connect to.
78
+ By default the name of the COM::Instantiable class’ name is used, taking the
79
+ last two double-colon-separated components and joining them with a dot. For
80
+ Word::Application, the program ID is “Word.Application”. The program ID can be
81
+ set by using the .program_id method:
82
+
83
+ class IDontCare::ForConventions < COM::Instantiable
84
+ program_id 'Word.Application'
85
+ end
86
+
87
+ The program ID can be accessed with the same method:
88
+
89
+ Word::Application.program_id # ⇒ 'Word.Application'
90
+
91
+ Connecting to an already running COM object is not done by default, but is
92
+ sometimes desirable: the COM object might take a long time to create, or some
93
+ common state needs to be accessed. If the default for a certain instantiable
94
+ COM object should be to connect, this can be done using the .connect method:
95
+
96
+ class Word::Application < COM::Instantiable
97
+ connect
98
+ end
99
+
100
+ If no running COM object is available, then a new COM object will be created in
101
+ its stead. Whether or not a class uses the connection method can be queried
102
+ with the .connect? method:
103
+
104
+ Word::Application.connect? # ⇒ true
105
+
106
+ Whether or not to load constants associated with an instantiable COM object is
107
+ set with the .constants method:
108
+
109
+ class Word::Application < COM::Instantiable
110
+ constants true
111
+ end
112
+
113
+ and can similarly be checked:
114
+
115
+ Word::Application.constants? # ⇒ true
116
+
117
+ Constants are loaded by default.
118
+
119
+ When an instance of the instantiable COM object is created, a check is run to
120
+ see if constants should be loaded and whether or not they already have been
121
+ loaded. If they should be loaded and they haven’t already been loaded,
122
+ they’re, you guessed it, loaded. The constants are added to the module
123
+ containing the COM::Instantiable. Thus, for Word::Application, the Word module
124
+ will contain all the constants. Whether or not the constants have already been
125
+ loaded can be checked with .constants_loaded?:
126
+
127
+ Word::Application.constants_loaded # ⇒ false
128
+
129
+ That concludes the class-level methods.
130
+
131
+ Let’s begin with the #connected? method among the instance-level methods. This
132
+ method queries whether or not this instance connected to an already running COM
133
+ object:
134
+
135
+ Word::Application.new.connected? # ⇒ false
136
+
137
+ This can be very important in determining how shutdown of a COM object should
138
+ be done. If you connected to an already COM object it might be foolish to shut
139
+ it down if someone else is using it.
140
+
141
+ The #initialize method takes a couple of options:
142
+
143
+ * connect: whether or not to connect to a running instance
144
+ * constants: whether or not to load constants
145
+
146
+ These options will, when given, override the class-level defaults.
147
+
148
+ ### Events ###
149
+
150
+ COM events are easily dealt with:
151
+
152
+ class Word::Application < COM::Instantiable
153
+ def initialize(options = {})
154
+ super
155
+ @events = COM::Events.new(com, 'ApplicationEvents',
156
+ 'OnQuit')
157
+ end
158
+
159
+ def quit(saving = Word::WdDoNotSaveChanges, *args)
160
+ @events.observe('OnQuit', proc{ com.quit saving, *args }) do
161
+ yield if block_given?
162
+ end
163
+ end
164
+ end
165
+
166
+ To tell you the truth this API sucks and will most likely be rewritten. The
167
+ reason that it is the way it is is that WIN32OLE, which COM wraps, sucks. It’s
168
+ event API is horrid and the implementation is buggy. It will keep every
169
+ registered event block in memory for ever, freeing neither the blocks nor the
170
+ COM objects that yield the events.
171
+
172
+ ### Errors ###
173
+
174
+ All errors generated by COM methods descend from COM::Error, except for those
175
+ cases where a Ruby error already exists. The following HRESULT error codes are
176
+ turned into Ruby errors:
177
+
178
+ HRESULT Error Code | Error Class
179
+ -------------------|------------
180
+ 0x80004001 | NotImplementedError
181
+ 0x80020005 | TypeError
182
+ 0x80020006 | NoMethodError
183
+ 0x8002000e | ArgumentError
184
+ 0x800401e4 | ArgumentError
185
+
186
+ There are also a couple of other HRESULT error codes that are turned into more
187
+ specific errors than COM::Error:
188
+
189
+ HRESULT Error Code | Error Class
190
+ -------------------|------------
191
+ 0x80020003 | MemberNotFoundError
192
+ 0x800401e3 | OperationUnavailableError
193
+
194
+ Finally, when a method results in any other error, a COM::MethodInvocationError
195
+ will be raised, which can be queried for the specifics, specifically #message,
196
+ #method, #server, #code, #hresult_code, and #hresult_message.
197
+
198
+ ### Pathname ###
199
+
200
+ The Pathname object receives an additional method, #to_com. This method is
201
+ useful for when you want to pass a Pathname object to a COM method. Simply
202
+ call #to_com to turn it into a String of the right encoding for COM:
203
+
204
+ Word::Application.new.documents.open(Pathname('a.docx').to_com)
205
+ # ⇒ Word::Document
206
+
207
+
208
+ ## Installation ##
209
+
210
+ Install COM with
211
+
212
+ % gem install com
213
+
214
+
215
+ ## License ##
216
+
217
+ You may use, copy and redistribute this library under the same [terms][1] as
218
+ Ruby itself.
219
+
220
+ [1]: http://www.ruby-lang.org/en/LICENSE.txt
221
+
222
+
223
+ ## Contributors ##
224
+
225
+ * Nikolai Weibull
@@ -0,0 +1,10 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'lookout/rake/tasks'
4
+ require 'yard'
5
+
6
+ Lookout::Rake::Tasks::Test.new
7
+ Lookout::Rake::Tasks::Gem.new
8
+ YARD::Rake::YardocTask.new do |t|
9
+ t.options.concat %w[--markup markdown]
10
+ end
@@ -0,0 +1,73 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'win32ole'
4
+ WIN32OLE.codepage = WIN32OLE::CP_UTF8
5
+
6
+ # COM is an object-oriented wrapper around WIN32OLE. COM makes it easy to add
7
+ # behavior to WIN32OLE objects, making them easier to work with from Ruby.
8
+ module COM
9
+ autoload :Error, 'com/error'
10
+ autoload :Events, 'com/events'
11
+ autoload :Instantiable, 'com/instantiable'
12
+ autoload :Object, 'com/object'
13
+ autoload :PatternError, 'com/patternerror'
14
+ autoload :Version, 'com/version'
15
+ autoload :Wrapper, 'com/wrapper'
16
+
17
+ class << self
18
+ # Gets the iconv character set equivalent of the current COM code page.
19
+ #
20
+ # @raise [RuntimeError] If no iconv charset is associated with the current
21
+ # COM codepage
22
+ # @return [String] The iconv character set
23
+ def charset
24
+ COMCodePageToIconvCharset[WIN32OLE.codepage] or
25
+ raise 'no iconv charset associated with current COM codepage: %s' %
26
+ WIN32OLE.codepage
27
+ end
28
+
29
+ # Connects to a running COM object.
30
+ #
31
+ # This method shouldn’t be used directly, but rather is used by
32
+ # COM::Object.
33
+ #
34
+ # @param [String] id The program ID of the COM object to connect to
35
+ # @raise [COM::Error] Any error that may have occurred while trying to
36
+ # connect
37
+ # @return [COM::MethodMissing] The running COM object wrapped in a
38
+ # COM::MethodMissing
39
+ def connect(id)
40
+ Wrapper.new(WIN32OLE.connect(id))
41
+ rescue WIN32OLERuntimeError => e
42
+ raise Error.from(e)
43
+ end
44
+
45
+ # Creates a new COM object.
46
+ #
47
+ # This method shouldn’t be used directly, but rather is used by
48
+ # COM::Object.
49
+ #
50
+ # @param [String] id The program ID of the COM object to create
51
+ # @param [String, nil] host The host of the program ID
52
+ # @raise [COM::Error] Any error that may have occurred while trying to
53
+ # create the COM object
54
+ # @return [COM::MethodMissing] The COM object wrapped in a COM::MethodMissing
55
+ def new(id, host = nil)
56
+ Wrapper.new(WIN32OLE.new(id, host))
57
+ rescue WIN32OLERuntimeError => e
58
+ raise Error.from(e)
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ # @private
65
+ COMCodePageToIconvCharset = {
66
+ WIN32OLE::CP_UTF8 => 'UTF-8'
67
+ }.freeze
68
+ end
69
+
70
+ require 'com/methodinvocationerror'
71
+ require 'com/pathname'
72
+ require 'com/standarderror'
73
+ require 'com/win32ole'
@@ -0,0 +1,44 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Superclass for COM-related errors. This class is meant to be subclassed an
4
+ # used for refinement of otherwise very unspecific COM errors.
5
+ #
6
+ # @see COM::StandardError
7
+ class COM::Error < RuntimeError
8
+ class << self
9
+ # Creates a COM::Error from _error_, with optional _backtrace_. _Error_
10
+ # should be an error raised by WIN32OLE.
11
+ #
12
+ # This is an internal method.
13
+ #
14
+ # @param [WIN32OLERuntimeError] error The error to create a new one from
15
+ # @param [Array<String>, nil] backtrace The backtrace to use for the new
16
+ # error
17
+ def from(error, backtrace = nil)
18
+ errors.find{ |replacement| replacement.replaces? error }.replace(error).tap{ |e|
19
+ e.set_backtrace backtrace if backtrace
20
+ }
21
+ end
22
+
23
+ # @private method used by {.from}.
24
+ def replaces?(error)
25
+ true
26
+ end
27
+
28
+ protected
29
+
30
+ def replace(error)
31
+ new(error.message)
32
+ end
33
+
34
+ private
35
+
36
+ def inherited(error)
37
+ errors.unshift error
38
+ end
39
+
40
+ def errors
41
+ @@errors ||= [self]
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,68 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Provides a simple wrapper around COM events. WIN32OLE provides no way of
4
+ # releasing an observer and thus causes an unbreakable chain of objects,
5
+ # causing uncollectable garbage. This class obviates most of the problems.
6
+ class COM::Events
7
+ ArgumentMissing = Object.new.freeze
8
+
9
+ # Creates a COM events wrapper for the COM object _com_’s _interface_.
10
+ # Optionally, any _events_ may be given as additional arguments.
11
+ #
12
+ # @param [WIN32OLE] com COM object implementing _interface_
13
+ # @param [String] interface Name of the COM interface that _com_ implements
14
+ # @param [Array<String>] events Names of events to register (see
15
+ # {#register})
16
+ def initialize(com, interface = nil)
17
+ @interface = WIN32OLE_EVENT.new(com, interface)
18
+ @events = {}
19
+ end
20
+
21
+ # Observe _event_ in _block_ during the execution of _during_.
22
+ #
23
+ # @param [String] event Name of the event to observe
24
+ # @param [Proc] during Block during which to observe _event_
25
+ # @yield [*args] Event arguments (specific for each event)
26
+ # @return The result of _during_
27
+ def observe(event, observer = ArgumentMissing)
28
+ if ArgumentMissing.equal? observer
29
+ register event, Proc.new
30
+ return self
31
+ end
32
+ return self unless observer
33
+ observer = observer.to_proc
34
+ register event, observer
35
+ return self unless block_given?
36
+ begin
37
+ yield
38
+ ensure
39
+ unobserve event, observer
40
+ end
41
+ self
42
+ end
43
+
44
+ def unobserve(event, observer = nil)
45
+ @events[event].delete observer if observer
46
+ if observer.nil? or @events[event].empty?
47
+ @interface.off_event event
48
+ @events.delete event
49
+ end
50
+ self
51
+ end
52
+
53
+ private
54
+
55
+ def register(event, observer)
56
+ if @events.include? event
57
+ @events[event] << observer
58
+ return
59
+ end
60
+ @interface.on_event event do |*args|
61
+ @events[event].reduce({}){ |result, o|
62
+ r = o.call(*args)
63
+ result.merge! r if Hash === r
64
+ }
65
+ end
66
+ @events[event] = [observer]
67
+ end
68
+ end
@@ -0,0 +1,135 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Represents instantiable COM objects. These are COM objects that we can
4
+ # connect to and create.
5
+ class COM::Instantiable < COM::Object
6
+ class << self
7
+ # Gets or sets the COM program ID.
8
+ #
9
+ # If no program ID has explicitly been set, one based on the name of this
10
+ # class and its containing module. For example, A::B is turned into
11
+ # 'A.B'.
12
+ #
13
+ # @param [String,nil] id Program ID to, if given, use
14
+ # @return [String] The set or automatically generated program ID
15
+ # @raise [ArgumentError] If no program ID has been set and one can’t be
16
+ # automatically generated
17
+ def program_id(id = nil)
18
+ @id = id if id
19
+ return @id if defined? @id
20
+ raise ArgumentError,
21
+ 'no automatic COM program ID for class available: %s' % self unless
22
+ matches = /^.*?([^:]+)::([^:]+)$/.match(name)
23
+ @id = '%s.%s' % matches[1..2]
24
+ end
25
+
26
+ # Marks this class as trying to connect to already running instances of COM
27
+ # objects.
28
+ #
29
+ # @return true
30
+ def connect
31
+ @connect = true
32
+ end
33
+
34
+ # Queries whether or not this class tries to connect to already running
35
+ # instances of COM objects.
36
+ #
37
+ # The default is false.
38
+ #
39
+ # @return Whether or not this class tries to connect to already running
40
+ # instances of COM objects
41
+ #
42
+ # @see #connect
43
+ def connect?
44
+ @connect ||= false
45
+ end
46
+
47
+ # Sets whether or not this class tries to load constants when connecting to
48
+ # or creating a COM object.
49
+ #
50
+ # @param [Boolean] constants Whether or not to load constans
51
+ # @return [Boolean] Whether or not to load constants
52
+ def constants(constants)
53
+ @constants = constants
54
+ end
55
+
56
+ # Queries whether or not this class tries to load constans when connecting
57
+ # to or creating a COM object.
58
+ #
59
+ # The default is true.
60
+ #
61
+ # @return Whether or not to load constants
62
+ def constants?
63
+ return true unless defined? @constants
64
+ @constants
65
+ end
66
+
67
+ # Loads constants associated with COM object _com_. This is an internal
68
+ # method that shouldn’t be called outside of this class.
69
+ #
70
+ # @param [WIN32OLE] com COM object to load constants from
71
+ # @return [Boolean] Whether or not any constants where loaded
72
+ def load_constants(com)
73
+ return if constants_loaded?
74
+ modul = nesting[-2]
75
+ com.load_constants modul
76
+ @constants_loaded = true
77
+ end
78
+
79
+ # Queries whether constants have already been loaded for this class.
80
+ #
81
+ # @return Whether or not constants have already been loaded for this class
82
+ def constants_loaded?
83
+ @constants_loaded ||= false
84
+ end
85
+
86
+ private
87
+
88
+ # Gets the nesting of modules that lead up to this class.
89
+ #
90
+ # @return [Array<Module>] Modules that nest this class
91
+ def nesting
92
+ result = []
93
+ name.split(/::/).inject(Module) do |modul, name|
94
+ modul.const_get(name).tap{ |c| result << c }
95
+ end
96
+ result
97
+ end
98
+ end
99
+
100
+ # Connects to or creates a new instance of a COM object.
101
+ #
102
+ # @option options [Boolean] :connect (false) Whether or not to connect to a
103
+ # running instance (see {.connect})
104
+ # @option options [Boolean] :constants (true) Whether or not to load
105
+ # constants associated with the COM object (see {.constants})
106
+ def initialize(options = {})
107
+ @connected = false
108
+ connect if options.fetch(:connect, self.class.connect?)
109
+ self.com = COM.new(self.class.program_id) unless connected?
110
+ self.class.load_constants(com) if
111
+ options.fetch(:constants, self.class.constants?)
112
+ end
113
+
114
+ # Queries whether or not an already running COM object was connected to.
115
+ # This is useful when deciding whether or not to close down the COM object
116
+ # when you’re done with it.
117
+ #
118
+ # @return Whether or not an already running COM object was connected to
119
+ def connected?
120
+ @connected
121
+ end
122
+
123
+ def inspect
124
+ '#<%s%s>' % [self.class, connected? ? ' connected' : '']
125
+ end
126
+
127
+ private
128
+
129
+ # Try to connect to a running COM object.
130
+ def connect
131
+ self.com = COM.connect(self.class.program_id)
132
+ @connected = true
133
+ rescue COM::OperationUnavailableError
134
+ end
135
+ end