ginjo-rfm 1.4.2

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2007 Six Fried Rice, LLC and Mufaddal Khumr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,329 @@
1
+ # ginjo-rfm
2
+
3
+ Rfm is a Ruby/Filemaker adapter - a ruby gem that allows scripts and applications to exchange commands and data with Filemaker Pro using Filemaker's XML interface. Ginjo-rfm picks up from the lardawge-rfm gem and continues to refine code and fix bugs. It also adds minor enhancements like server timeout and value-list alternate display. To read more about Rfm, see the info at [Sixfriedrice](http://sixfriedrice.com/wp/products/rfm/), or check out the [RDoc](http://rdoc.info/projects/lardawge/rfm) courtesey of Larry Sprock.
4
+
5
+ Rfm was primarily designed by Six Fried Rice co-founder Geoff Coffey.
6
+
7
+ Other lead contributors:
8
+
9
+ * Mufaddal Khumri helped architect Rfm in the most ruby-like way possible. He also contributed the outstanding error handling code and a comprehensive hierarchy of error classes.
10
+ * Atsushi Matsuo was an early Rfm tester, and provided outstanding feedback, critical code fixes, and a lot of web exposure.
11
+ * Jesse Antunes helped ensure that Rfm is stable and functional.
12
+ * Larry Sprock added ssl support and switched the xml parser to a much faster Nokogiri.
13
+
14
+ Documentation
15
+
16
+ * Homepage: <http://sixfriedrice.com/wp/products/rfm/>
17
+ * Rdoc location: <http://rubydoc.info/gems/lardawge-rfm/1.4.1.2/frames>
18
+ * Discussion: <http://groups.google.com/group/rfmcommunity>
19
+
20
+ An incomplete list of recent features added since rfm 1.0.0.
21
+
22
+ * SSL support
23
+ * Nokogiri xml parser
24
+ * Display-vs-data value lists
25
+ * Connection timeout
26
+ * Metadata support re-introduced
27
+
28
+ Partial roadmap to the future
29
+
30
+ * ActiveModel compatibility
31
+ * Alternative XML parsers
32
+ * More tests
33
+
34
+
35
+ ## Installation
36
+
37
+ Rfm depends on Nokogiri gem, which installs executables requiring C compilation. Make sure you have a C compiler installed, ruby development headers, and Nokogiri's pre-requisite libxml2 & libxslt (or Xcode on OS X). For help installing [Nokogiri](http://nokogiri.org/), see their helpful [tutorial](http://nokogiri.org/tutorials/installing_nokogiri.html). Future versions of ginjo-rfm will offer alternative XML parsing options, hopefully making it easier to get up and running.
38
+
39
+ Terminal:
40
+
41
+ bash
42
+ gem install ginjo-rfm
43
+
44
+ Once the gem is installed, you can use rfm in your ruby scripts by requiring it:
45
+
46
+ ruby
47
+ require 'rubygems'
48
+ require 'rfm'
49
+
50
+ ### In Rails >= 3.0
51
+
52
+ In the Gemfile:
53
+
54
+ ruby
55
+ gem 'ginjo-rfm'
56
+
57
+ ### Edge
58
+
59
+ #### ActiveModel support.
60
+
61
+ class Account < Rfm::Base
62
+ config :layout=>'account_xml'
63
+ before_create :encrypt_password
64
+ validates :email, :presence => true
65
+ validates :username, :presence => true
66
+ attr_accessor :password
67
+ end
68
+
69
+ @account = Account.new(:username=>'bill', :password=>'pass')
70
+ @account.email = 'my@email.com'
71
+ @account.save!
72
+
73
+ #### Multiple backend xml parsers. Using ActiveSupport::XmlMini to support Nokogiri, Libxml-ruby, Rexml.
74
+
75
+ Rfm::Server.new(:backend => :nokogiri)
76
+
77
+ Rfm::Server.new(:backend => :rexml)
78
+
79
+ Try it out:
80
+
81
+ gemfile
82
+ gem 'ginjo-rfm', :git=>'git://github.com/ginjo/rfm.git', :branch=>'edge'
83
+
84
+ ## Connecting
85
+
86
+ IMPORTANT:SSL and Certificate verification are on by default. Please see Server#new in rdocs for explanation and setup.
87
+ You connect with the Rfm::Server object. This little buddy will be your window into FileMaker data.
88
+
89
+ ruby
90
+ require 'rfm'
91
+
92
+ my_server = Rfm::Server.new(
93
+ :host => 'myservername',
94
+ :account_name => 'user',
95
+ :password => 'pw',
96
+ :ssl => false
97
+ )
98
+
99
+ if your web publishing engine runs on a port other than 80, you can provide the port number as well:
100
+
101
+ ruby
102
+ my_server = Rfm::Server.new(
103
+ :host => 'myservername',
104
+ :account_name => 'user',
105
+ :password => 'pw',
106
+ :port => 8080,
107
+ :ssl => false,
108
+ :root_cert => false
109
+ )
110
+
111
+ ## Databases and Layouts
112
+
113
+ All access to data in FileMaker's XML interface is done through layouts, and layouts live in databases. The Rfm::Server object has a collection of databases called 'db'. So to get ahold of a database called "My Database", you can do this:
114
+
115
+ ruby
116
+ my_db = my_server.db["My Database"]
117
+
118
+ As a convenience, you can do this too:
119
+
120
+ ruby
121
+ my_db = my_server["My Database"]
122
+
123
+ Finally, if you want to introspect the server and find out what databases are available, you can do this:
124
+
125
+ ruby
126
+ all_dbs = my_server.db.all
127
+
128
+ In any case, you get back Rfm::Database objects. A database object in turn has a property called "layout":
129
+
130
+ ruby
131
+ my_layout = my_db.layout["My Layout"]
132
+
133
+ Again, for convenience:
134
+
135
+ ruby
136
+ my_layout = my_db["My Layout"]
137
+
138
+ And to get them all:
139
+
140
+ ruby
141
+ all_layouts = my_db.layout.all
142
+
143
+ Bringing it all together, you can do this to go straight from a server to a specific layout:
144
+
145
+ ruby
146
+ my_layout = my_server["My Database"]["My Layout"]
147
+
148
+ ## Working with Layouts
149
+
150
+ Once you have a layout object, you can start doing some real work. To get every record from the layout:
151
+
152
+ ruby
153
+ my_layout.all # be careful with this
154
+
155
+ To get a random record:
156
+
157
+ ruby
158
+ my_layout.any
159
+
160
+ To find every record with "Arizona" in the "State" field:
161
+
162
+ ruby
163
+ my_layout.find({"State" => "Arizona"})
164
+
165
+ To add a new record with my personal info:
166
+
167
+ ruby
168
+ my_layout.create({
169
+ :first_name => "Geoff",
170
+ :last_name => "Coffey",
171
+ :email => "gwcoffey@gmail.com"}
172
+ )
173
+
174
+ Notice that in this case I used symbols instead of strings for the hash keys. The API will accept either form, so if your field names don't have whitespace or punctuation, you might prefer the symbol notation.
175
+
176
+ To edit the record whos recid (filemaker internal record id) is 200:
177
+
178
+ ruby
179
+ my_layout.edit(200, {:first_name => 'Mamie'})
180
+
181
+ Note: See the "Record Objects" section below for more on editing records.
182
+
183
+ To delete the record whose recid is 200:
184
+
185
+ ruby
186
+ my_layout.delete(200)
187
+
188
+ All of these methods return an Rfm::Result::ResultSet object (see below), and every one of them takes an optional parameter (the very last one) with additional options. For example, to find just a page full of records, you can do this:
189
+
190
+ ruby
191
+ my_layout.find({:state => "AZ"}, {:max_records => 10, :skip_records => 100})
192
+
193
+ For a complete list of the available options, see the "expand_options" method in the Rfm::Server object in the file named rfm_command.rb.
194
+
195
+ Finally, if filemaker returns an error when executing any of these methods, an error will be raised in your ruby script. There is one exception to this, though. If a find results in no records being found (FileMaker error # 401) I just ignore it and return you a ResultSet with zero records in it. If you prefer an error in this case, add :raise_on_401 => true to the options you pass the Rfm::Server when you create it.
196
+
197
+
198
+ ## ResultSet and Record Objects
199
+
200
+ Any method on the Layout object that returns data will return a ResultSet object. Rfm::Result::ResultSet is a subclass of Array, so first and foremost, you can use it like any other array:
201
+
202
+ ruby
203
+ my_result = my_layout.any
204
+ my_result.size # returns '1'
205
+ my_result[0] # returns the first record (an Rfm::Result::Record object)
206
+
207
+ The ResultSet object also tells you information about the fields and portals in the result. ResultSet#fields and ResultSet#portals are both standard ruby hashes, with strings for keys. The fields hash has Rfm::Result::Field objects for values. The portals hash has another hash for its values. This nested hash is the fields on the portal. This would print out all the field names:
208
+
209
+ ruby
210
+ my_result.fields.each { |name, field| puts name }
211
+
212
+ This would print out the tables each portal on the layout is associated with. Below each table name, and indented, it will print the names of all the fields on each portal.
213
+
214
+ ruby
215
+ my_result.portals.each { |table, fields|
216
+ puts "table: #{table}"
217
+ fields.each { |name, field| puts "\t#{name}"}
218
+ }
219
+
220
+ But most importantly, the ResultSet contains record objects. Rfm::Result::Record is a subclass of Hash, so it can be used in many standard ways. This code would print the value in the 'first_name' field in the first record of the ResultSet:
221
+
222
+ ruby
223
+ my_record = my_result[0]
224
+ puts my_record["first_name"]
225
+
226
+ As a convenience, if your field names are valid ruby method names (ie, they don't have spaces or odd punctuation in them), you can do this instead:
227
+
228
+ ruby
229
+ puts my_record.first_name
230
+
231
+ Since ResultSets are arrays and Records are hashes, you can take advantage of Ruby's wonderful expressiveness. For example, to get a comma-separated list of the full names of all the people in California, you could do this:
232
+
233
+ ruby
234
+ my_layout.find(:state => 'CA').collect {|rec| "#{rec.first_name} #{rec.last_name}"}.join(", ")
235
+
236
+ Record objects can also be edited:
237
+
238
+ ruby
239
+ my_record.first_name = 'Isabel'
240
+
241
+ Once you have made a series of edits, you can save them back to the database like this:
242
+
243
+ ruby
244
+ my_record.save
245
+
246
+ The save operation causes the record to be reloaded from the database, so any changes that have been made outside your script will also be picked up after the save.
247
+
248
+ If you want to detect concurrent modification, you can do this instead:
249
+
250
+ ruby
251
+ my_record.save_if_not_modified
252
+
253
+ This version will refuse to update the database and raise an error if the record was modified after it was loaded but before it was saved.
254
+
255
+ Record objects also have portals. While the portals in a ResultSet tell you about the tables and fields the portals show, the portals in a Record have the actual data. For example, if an Order record has Line Item records, you could do this:
256
+
257
+ ruby
258
+ my_order = order_layout.any[0] # the [0] is important!
259
+ my_lines = my_order.portals["Line Items"]
260
+
261
+ At the end of the previous block of code, my_lines is an array of Record objects. In this case, they are the records in the "Line Items" portal for the particular order record. You can then operate on them as you would any other record.
262
+
263
+ NOTE: Fields on a portal have the table name and the "::" stripped off of their names if they belong to the table the portal is tied to. In other words, if our "Line Items" portal includes a quantity field and a price field, you would do this:
264
+
265
+ ruby
266
+ my_lines[0]["Quantity"]
267
+ my_lines[0]["Price"]
268
+
269
+ You would NOT do this:
270
+
271
+ ruby
272
+ my_lines[0]["Line Items::Quantity"]
273
+ my_lines[0]["Line Items::Quantity"]
274
+
275
+ My feeling is that the table name is redundant and cumbersome if it is the same as the portal's table. This is also up for debate.
276
+
277
+ Again, you can string things together with Ruby. This will calculate the total dollar amount of the order:
278
+
279
+ ruby
280
+ total = 0.0
281
+ my_order.portals["Line Items"].each {|line| total += line.quantity * line.price}
282
+
283
+ ## Data Types
284
+
285
+ FileMaker's field types are coerced to Ruby types thusly:
286
+
287
+ Text Field -> String object
288
+ Number Field -> BigDecimal object # see below
289
+ Date Field -> Date object
290
+ Time Field -> DateTime object # see below
291
+ TimeStamp Field -> DateTime object
292
+ Container Field -> URI object
293
+
294
+ FileMaker's number field is insanely robust. The only data type in ruby that can handle the same magnitude and precision of a FileMaker number is Ruby's BigDecimal. (This is an extension class, so you have to require 'bigdecimal' to use it yourself). Unfortuantely, BigDecimal is not a "normal" ruby numeric class, so it might be really annoying that your tiny filemaker numbers have to go this route. This is a great topic for debate.
295
+
296
+ Also, Ruby doesn't have a Time type that stores just a normal time (with no date attached). The Time class in ruby is a lot like DateTime, or a Timestamp in FileMaker. When I get a Time field from FileMaker, I turn it into a DateTime object, and set its date to the oldest date Ruby supports. You can still compare these in all the normal ways, so this should be fine, but it will look weird if you, ie, to_s one and see an odd date attached to your time.
297
+
298
+ Finally, container fields will come back as URI objects. You can:
299
+
300
+ - use Net::HTTP to download the contents of the container field using this URI
301
+ - to_s the URI and use it as the src attribute of an HTML image tag
302
+ - etc...
303
+
304
+ Specifically, the URI refers to the _contents_ of the container field. When accessed, the file, picture, or movie in the field will be downloaded.
305
+
306
+ ## Troubleshooting
307
+
308
+ There are two cheesy methods to help track down problems. When you create a server object, you can provide two additional optional parameters:
309
+
310
+ :log_actions
311
+ When this is 'true' your script will write every URL it sends to the web publishing engine to standard out. For the rails users, this means the action url will wind up in your WEBrick or Mongrel log. If you can't make sense of what you're getting, you might try copying the URL into your browser to see what is actually coming back from FileMaker.
312
+
313
+ :log_responses
314
+ When this is 'true' your script will dump the actual response it got from FileMaker to standard out (again, in rails, check your logs).
315
+
316
+ So, for an annoying, but detailed load of output, make a connection like this:
317
+
318
+ ruby
319
+ my_server => Rfm::Server.new(
320
+ :host => 'myservername',
321
+ :account_name => 'user',
322
+ :password => 'pw',
323
+ :log_actions => true,
324
+ :log_responses => true
325
+ )
326
+
327
+ ## Copyright
328
+
329
+ Copyright (c) 2007 Six Fried Rice, LLC and Mufaddal Khumr. See LICENSE for details.
data/lib/rfm.rb ADDED
@@ -0,0 +1,32 @@
1
+ path = File.expand_path(File.dirname(__FILE__))
2
+ $:.unshift(path) unless $:.include?(path)
3
+
4
+ require path + '/rfm/utilities/case_insensitive_hash'
5
+ require path + '/rfm/utilities/factory'
6
+ require path + '/rfm/version.rb'
7
+
8
+ module Rfm
9
+
10
+ if $0.to_s.match(/irb|rails|bundle/) # was ENV['_']
11
+ puts "Using gem ginjo-rfm version: #{VERSION}"
12
+ end
13
+
14
+ class CommunicationError < StandardError; end
15
+ class ParameterError < StandardError; end
16
+ class AuthenticationError < StandardError; end
17
+
18
+ autoload :Error, 'rfm/error'
19
+ autoload :Server, 'rfm/server'
20
+ autoload :Database, 'rfm/database'
21
+ autoload :Layout, 'rfm/layout'
22
+ autoload :Resultset, 'rfm/resultset'
23
+ autoload :Record, 'rfm/record'
24
+
25
+ module Metadata
26
+ autoload :Script, 'rfm/metadata/script'
27
+ autoload :Field, 'rfm/metadata/field'
28
+ autoload :FieldControl, 'rfm/metadata/field_control'
29
+ autoload :ValueListItem, 'rfm/metadata/value_list_item'
30
+ end
31
+
32
+ end
@@ -0,0 +1,96 @@
1
+ module Rfm
2
+ # The Database object represents a single FileMaker Pro database. When you retrieve a Database
3
+ # object from a server, its account name and password are set to the account name and password you
4
+ # used when initializing the Server object. You can override this of course:
5
+ #
6
+ # myDatabase = myServer["Customers"]
7
+ # myDatabase.account_name = "foo"
8
+ # myDatabase.password = "bar"
9
+ #
10
+ # =Accessing Layouts
11
+ #
12
+ # All interaction with FileMaker happens through a Layout object. You can get a Layout object
13
+ # from the Database object like this:
14
+ #
15
+ # myLayout = myDatabase["Details"]
16
+ #
17
+ # This code gets the Layout object representing the layout called Details in the database.
18
+ #
19
+ # Note: RFM does not talk to the server when you retrieve a Layout object in this way. Instead, it
20
+ # simply assumes you know what you're talking about. If the layout you specify does not exist, you
21
+ # will get no error at this point. Instead, you'll get an error when you use the Layout object methods
22
+ # to talk to FileMaker. This makes debugging a little less convenient, but it would introduce too much
23
+ # overhead to hit the server at this point.
24
+ #
25
+ # The Database object has a +layout+ attribute that provides alternate access to Layout objects. It acts
26
+ # like a hash of Layout objects, one for each accessible layout in the database. So, for example, you
27
+ # can do this if you want to print out a list of all layouts:
28
+ #
29
+ # myDatabase.layout.each {|layout|
30
+ # puts layout.name
31
+ # }
32
+ #
33
+ # The Database::layout attribute is actually a LayoutFactory object, although it subclasses hash, so it
34
+ # should work in all the ways you expect. Note, though, that it is completely empty until the first time
35
+ # you attempt to access its elements. At that (lazy) point, it hits FileMaker, loads in the list of layouts,
36
+ # and constructs a Layout object for each one. In other words, it incurrs no overhead until you use it.
37
+ #
38
+ # =Accessing Scripts
39
+ #
40
+ # If for some reason you need to enumerate the scripts in a database, you can do so:
41
+ #
42
+ # myDatabase.script.each {|script|
43
+ # puts script.name
44
+ # }
45
+ #
46
+ # The Database::script attribute is actually a ScriptFactory object, although it subclasses hash, so it
47
+ # should work in all the ways you expect. Note, though, that it is completely empty until the first time
48
+ # you attempt to access its elements. At that (lazy) point, it hits FileMaker, loads in the list of scripts,
49
+ # and constructs a Script object for each one. In other words, it incurrs no overhead until you use it.
50
+ #
51
+ # Note: You don't need a Script object to _run_ a script (see the Layout object instead).
52
+ #
53
+ # =Attributes
54
+ #
55
+ # In addition to the +layout+ attribute, Server has a few other useful attributes:
56
+ #
57
+ # * *server* is the Server object this database comes from
58
+ # * *name* is the name of this database
59
+ # * *state* is a hash of all server options used to initialize this server
60
+ class Database
61
+
62
+ # Initialize a database object. You never really need to do this. Instead, just do this:
63
+ #
64
+ # myServer = Rfm::Server.new(...)
65
+ # myDatabase = myServer["Customers"]
66
+ #
67
+ # This sample code gets a database object representing the Customers database on the FileMaker server.
68
+ def initialize(name, server)
69
+ @name = name
70
+ @server = server
71
+ @account_name = server.state[:account_name] or ""
72
+ @password = server.state[:password] or ""
73
+ @layout = Rfm::Factory::LayoutFactory.new(server, self)
74
+ @script = Rfm::Factory::ScriptFactory.new(server, self)
75
+ end
76
+
77
+ attr_reader :server, :name, :account_name, :password, :layout, :script
78
+ attr_writer :account_name, :password
79
+
80
+ # Access the Layout object representing a layout in this database. For example:
81
+ #
82
+ # myDatabase['Details']
83
+ #
84
+ # would return a Layout object representing the _Details_
85
+ # layout in the database.
86
+ #
87
+ # Note: RFM never talks to the server until you perform an action. The Layout object
88
+ # returned is created on the fly and assumed to refer to a valid layout, but you will
89
+ # get no error at this point if the layout you specify doesn't exist. Instead, you'll
90
+ # receive an error when you actually try to perform some action it.
91
+ def [](layout_name)
92
+ self.layout[layout_name]
93
+ end
94
+
95
+ end
96
+ end