com 0.3.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.
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