rfm 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,224 @@
1
+ This documentation is hastily thrown together and probably contains lots of errors. Needless to say, this will need to be documented better soon :)
2
+
3
+ Code Samples
4
+ ============
5
+
6
+ To access the API you need to require the right pieces. The easiest way to do this is to:
7
+
8
+ require 'rfm/rfm'
9
+
10
+ near the top of your script. This will cause everhting in the API to be loaded.
11
+
12
+ Connecting
13
+ ----------
14
+
15
+ You connect with the Rfm::Server object. This little buddy will be your window into FileMaker data.
16
+
17
+ require 'rfm/rfm'
18
+
19
+ my_server = Rfm::Server.new(
20
+ :host => 'myservername',
21
+ :username => 'user',
22
+ :password => 'pw'
23
+ )
24
+
25
+ if your web publishing engine runs on a port other than 80, you can provide the port number as well:
26
+
27
+ my_server = Rfm::Server.new(
28
+ :host => 'myservername',
29
+ :username => 'user',
30
+ :password => 'pw',
31
+ :port => 8080
32
+ )
33
+
34
+ Databases and Layouts
35
+ ---------------------
36
+
37
+ 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:
38
+
39
+ my_db = my_server.db["My Database"]
40
+
41
+ As a convenience, you can do this too:
42
+
43
+ my_db = my_server["My Database"]
44
+
45
+ Finally, if you want to introspect the server and find out what databases are available, you can do this:
46
+
47
+ all_dbs = my_server.db.all
48
+
49
+ In any case, you get back Rfm::Database objects. A database object in turn has a property called "layout":
50
+
51
+ my_layout = my_db.layout["My Layout"]
52
+
53
+ Again, for convenience:
54
+
55
+ my_layout = my_db["My Layout"]
56
+
57
+ And to get them all:
58
+
59
+ all_layouts = my_db.layout.all
60
+
61
+ Bringing it all together, you can do this to go straight from a server to a specific layout:
62
+
63
+ my_layout = my_server["My Database"]["My Layout"]
64
+
65
+ NOTE: for the ruby experts, Rfm::Server#db is an Rfm::Factory::DatabaseFactory object, which is just a fancied up Hash. So anything you can do with a hash, you can also do with my_server.db. The same goes for my_db.layouts, which is an Rfm::Factory::LayoutFactory.
66
+
67
+
68
+ Working with Layouts
69
+ --------------------
70
+
71
+ Once you have a layout object, you can start doing some real work. To get every record from the layout:
72
+
73
+ my_layout.all # be careful with this
74
+
75
+ To get a random record:
76
+
77
+ my_layout.any
78
+
79
+ To find every record with "Arizona" in the "State" field:
80
+
81
+ my_layout.find({"State" => "Arizona"})
82
+
83
+ To add a new record with my personal info:
84
+
85
+ my_layout.create({
86
+ :first_name => "Geoff",
87
+ :last_name => "Coffey",
88
+ :email => "gwcoffey@gmail.com"}
89
+ )
90
+
91
+ 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.
92
+
93
+ To edit the record whos recid (filemaker internal record id) is 200:
94
+
95
+ my_layout.edit(200, {:first_name => 'Mamie'})
96
+
97
+ Note: See the "Record Objects" section below for more on editing records.
98
+
99
+ To delete the record whose recid is 200:
100
+
101
+ my_layout.delete(200)
102
+
103
+ 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:
104
+
105
+ my_layout.find({:state => "AZ"}, {:max_records => 10, :skip_records => 100})
106
+
107
+ 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.
108
+
109
+ 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. This is open for debate...
110
+
111
+
112
+ ResultSet and Record Objects
113
+ ----------------------------
114
+
115
+ 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:
116
+
117
+ my_result = my_layout.any
118
+ my_result.size # returns '1'
119
+ my_result[0] # returns the first record (an Rfm::Result::Record object)
120
+
121
+ 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:
122
+
123
+ my_result.fields.each { |name, field| puts name }
124
+
125
+ 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.
126
+
127
+ my_result.portals.each { |table, fields|
128
+ puts "table: #{table}"
129
+ fields.each { |name, field| puts "\t#{name}"}
130
+ }
131
+
132
+ 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:
133
+
134
+ my_record = my_result[0]
135
+ puts my_record["first_name"]
136
+
137
+ 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:
138
+
139
+ puts my_record.first_name
140
+
141
+ 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:
142
+
143
+ my_layout.find({:state => 'CA'}).collect {|rec| "#{rec.first_name} #{rec.last_name}"}.join(", ")
144
+
145
+ Record objects can also be edited:
146
+
147
+ my_record.first_name = 'Isabel'
148
+
149
+ Once you have made a series of edits, you can save them back to the database like this:
150
+
151
+ my_record.save
152
+
153
+ 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.
154
+
155
+ If you want to detect concurrent modification, you can do this instead:
156
+
157
+ my_record.save_if_not_modified
158
+
159
+ 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.
160
+
161
+ 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:
162
+
163
+ my_order = order_layout.any[0] # the [0] is important!
164
+ my_lines = my_order.portals["Line Items"]
165
+
166
+ 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.
167
+
168
+ 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:
169
+
170
+ my_lines[0]["Quantity"]
171
+ my_lines[0]["Price"]
172
+
173
+ You would NOT do this:
174
+
175
+ my_lines[0]["Line Items::Quantity"]
176
+ my_lines[0]["Line Items::Quantity"]
177
+
178
+ 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.
179
+
180
+ Again, you can string things together with Ruby. This will calculate the total dollar amount of the order:
181
+
182
+ total = 0.0
183
+ my_order.portals["Line Items"].each {|line| total += line.quantity * line.price}
184
+
185
+ I intend to add a 'delete' method to the Record class as well, but I haven't done this yet. Finally, should you be able to create new records using the Record class? I'm not sure...lots of points to consider on this.
186
+
187
+
188
+ Data Types
189
+ ----------
190
+
191
+ FileMaker's field types are coerced to Ruby types thusly:
192
+
193
+ Text Field -> String object
194
+ Number Field -> BigDecimal object # see below
195
+ Date Field -> Date object
196
+ Time Field -> DateTime object # see below
197
+ TimeStamp Field -> DateTime object
198
+
199
+ 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.
200
+
201
+ 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.
202
+
203
+ Troubleshooting
204
+ ---------------
205
+
206
+ There are two cheesy methods to help track down problems. When you create a server object, you can provide two additional optional parameters:
207
+
208
+ :log_actions
209
+ 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.
210
+
211
+ :log_responses
212
+ When this is 'true' your script will dump the actual response it got from FileMaker to standard out (again, in rails, check your logs).
213
+
214
+ So, for an annoying, but detailed load of output, make a connection like this:
215
+
216
+ my_server = Rfm::Server.new(
217
+ :host => 'myservername',
218
+ :username => 'user',
219
+ :password => 'pw',
220
+ :log_actions => true,
221
+ :log_responses => true
222
+ )
223
+
224
+ These options will change in the future. They're only there now as a quick-easy way for me to track down problems. I'm open to suggestions for what kind of loggin options will make sense in the final release.
data/lib/rfm.rb ADDED
@@ -0,0 +1,5 @@
1
+ $: << File.expand_path(File.dirname(__FILE__))
2
+
3
+ require 'rfm_command'
4
+ require 'rfm_result'
5
+ require 'rfm_factory'
@@ -0,0 +1,173 @@
1
+ require 'net/http'
2
+ require 'rexml/document'
3
+
4
+ module Rfm
5
+
6
+ class Server
7
+
8
+ def initialize(options)
9
+ @state = {
10
+ :host => 'localhost',
11
+ :port => 80,
12
+ :username => '',
13
+ :password => '',
14
+ :log_actions => false
15
+ }.merge(options)
16
+
17
+ @host_name = @state[:host]
18
+ @port = @state[:port]
19
+ @username = @state[:username]
20
+ @password = @state[:password]
21
+
22
+ @db = Rfm::Factory::DbFactory.new(self)
23
+ end
24
+
25
+ def [](dbname)
26
+ self.db[dbname]
27
+ end
28
+
29
+ attr_reader :db
30
+
31
+ def do_action(action, args, options = {})
32
+ post = args.merge(expand_options(options)).merge({action => ''})
33
+
34
+ if @state[:log_actions] == true
35
+ qs = post.collect{|key,val| "#{key}=#{val}"}.join("&")
36
+ warn "http://#{@host_name}:#{@port}/fmi/xml/fmresultset.xml?#{qs}"
37
+ end
38
+
39
+ request = Net::HTTP::Post.new("/fmi/xml/fmresultset.xml")
40
+ request.basic_auth(@username, @password)
41
+ request.set_form_data(post)
42
+ result = Net::HTTP.start(@host_name, @port) { |http|
43
+ http.request(request)
44
+ }
45
+
46
+ if @state[:log_responses] == true
47
+ puts result
48
+ end
49
+
50
+ result
51
+ end
52
+
53
+ private
54
+
55
+ def expand_options(options)
56
+ result = {}
57
+ options.each {|key,value|
58
+ case key
59
+ when :max_records:
60
+ result['-max'] = value
61
+ when :skip_records:
62
+ result['-skip'] = value
63
+ when :sort_field:
64
+ result['-sortfield'] = value
65
+ when :sort_order:
66
+ result['-sortorder'] = value
67
+ when :post_script:
68
+ if value.class == Array
69
+ result['-script'] = value[0]
70
+ result['-script.param'] = value[1]
71
+ else
72
+ result['-script'] = value
73
+ end
74
+ when :pre_find_scripts:
75
+ if value.class == Array
76
+ result['-script.prefind'] = value[0]
77
+ result['-script.prefind.param'] = value[1]
78
+ else
79
+ result['-script.presort'] = value
80
+ end
81
+ when :pre_sort_script:
82
+ if value.class == Array
83
+ result['-script.presort'] = value[0]
84
+ result['-script.presort.param'] = value[1]
85
+ else
86
+ result['-script.presort'] = value
87
+ end
88
+ when :response_layout:
89
+ result['-lay.response'] = value
90
+ when :logical_operator:
91
+ result['-lop'] = value
92
+ else
93
+ raise "Invalid option: #{key} (are you using a string instead of a symbol?)"
94
+ end
95
+ }
96
+ result
97
+ end
98
+
99
+ end
100
+
101
+ class Database
102
+ def initialize(name, server)
103
+ @name = name
104
+ @server = server
105
+ @layout = Rfm::Factory::LayoutFactory.new(server, self)
106
+ @script = Rfm::Factory::ScriptFactory.new(server, self)
107
+ end
108
+
109
+ attr_reader :server, :name, :layout, :script
110
+
111
+ def [](layout_name)
112
+ self.layout[layout_name]
113
+ end
114
+
115
+ end
116
+
117
+ class Layout
118
+ def initialize(name, db)
119
+ @name = name
120
+ @db = db
121
+ end
122
+
123
+ attr_reader :name
124
+
125
+ def all(options = {})
126
+ get_records('-findall', {}, options)
127
+ end
128
+
129
+ def any(options = {})
130
+ get_records('-findany', {}, options)
131
+ end
132
+
133
+ def find(query_map, options = {})
134
+ get_records('-find', query_map, options)
135
+ end
136
+
137
+ def edit(recid, values, options = {})
138
+ get_records('-edit', {'-recid' => recid}.merge(values), options)
139
+ end
140
+
141
+ def create(values, options = {})
142
+ get_records('-new', values, options)
143
+ end
144
+
145
+ def delete(recid, options = {})
146
+ get_records('-delete', {'-recid' => recid}, options)
147
+ return nil
148
+ end
149
+
150
+ private
151
+
152
+ def get_records(action, extra_params = {}, options = {})
153
+ Rfm::Result::ResultSet.new(
154
+ @db.server.do_action(action, params().merge(extra_params), options).body,
155
+ self)
156
+ end
157
+
158
+ def params
159
+ {"-db" => @db.name, "-lay" => self.name}
160
+ end
161
+ end
162
+
163
+ class Script
164
+ def initialize(name, db)
165
+ @name = name
166
+ @db = db
167
+ end
168
+
169
+ attr_reader :name
170
+ end
171
+
172
+ end
173
+
data/lib/rfm_error.rb ADDED
@@ -0,0 +1,25 @@
1
+ module Rfm::Error
2
+ class FileMakerError < Exception
3
+ def self.instance(error_code)
4
+ case error_code
5
+ when 101 then return RecordMissingError.new
6
+ when 102 then return FieldMissingError.new
7
+ when 104 then return RelationshipMissingError.new
8
+ when 105 then return LayoutMissingError.new
9
+ else return FileMakerError.new
10
+ end
11
+ end
12
+ end
13
+
14
+ class RecordMissingError < FileMakerError
15
+ end
16
+
17
+ class FieldMissingError < FileMakerError
18
+ end
19
+
20
+ class RelationshipMissingError < FileMakerError
21
+ end
22
+
23
+ class LayoutMissingError < FileMakerError
24
+ end
25
+ end
@@ -0,0 +1,75 @@
1
+ module Rfm::Factory
2
+ class DbFactory < Hash
3
+
4
+ def initialize(server)
5
+ @server = server
6
+ @loaded = false
7
+ end
8
+
9
+ def [](dbname)
10
+ super or (self[dbname] = Rfm::Database.new(dbname, @server))
11
+ end
12
+
13
+ def all
14
+ if !@loaded
15
+ Rfm::Result::ResultSet.new(@server.do_action('-dbnames', {}).body).each {|record|
16
+ name = record['DATABASE_NAME']
17
+ self[name] = Rfm::Database.new(name, @server) if self[name] == nil
18
+ }
19
+ @loaded = false
20
+ end
21
+ self.values
22
+ end
23
+
24
+ end
25
+
26
+ class LayoutFactory < Hash
27
+
28
+ def initialize(server, database)
29
+ @server = server
30
+ @database = database
31
+ @loaded = false
32
+ end
33
+
34
+ def [](layout_name)
35
+ super or (self[layout_name] = Rfm::Layout.new(layout_name, @database))
36
+ end
37
+
38
+ def all
39
+ if !@loaded
40
+ Rfm::Result::ResultSet.new(@server.do_action('-layoutnames', {"-db" => @database.name}).body).each {|record|
41
+ name = record['LAYOUT_NAME']
42
+ self[name] = Rfm::Layout.new(name, @database) if self[name] == nil
43
+ }
44
+ @loaded = true
45
+ end
46
+ self.values
47
+ end
48
+
49
+ end
50
+
51
+ class ScriptFactory < Hash
52
+
53
+ def initialize(server, database)
54
+ @server = server
55
+ @database = database
56
+ @loaded = false
57
+ end
58
+
59
+ def [](script_name)
60
+ super or (self[script_name] = Rfm::Script.new(script_name, @database))
61
+ end
62
+
63
+ def all
64
+ if !@loaded
65
+ Rfm::Result::ResultSet.new(@server.do_action('-scriptnames', {"-db" => @database.name}).body).each {|record|
66
+ name = record['SCRIPT_NAME']
67
+ self[name] = Rfm::Script.new(name, @database) if self[name] == nil
68
+ }
69
+ @loaded = true
70
+ end
71
+ self.values
72
+ end
73
+
74
+ end
75
+ end
data/lib/rfm_result.rb ADDED
@@ -0,0 +1,156 @@
1
+ require 'bigdecimal'
2
+
3
+ module Rfm::Result
4
+
5
+ class ResultSet < Array
6
+ def initialize(fmresultset, layout = nil)
7
+ @resultset = nil
8
+ @layout = layout
9
+ @fields = {}
10
+ @portals = {}
11
+
12
+ doc = REXML::Document.new(fmresultset)
13
+ root = doc.root
14
+
15
+ # check for errors
16
+ error = root.elements['error'].attributes['code'].to_i
17
+ raise "Error #{error} occurred while processing the request" if error != 0 && error != 401
18
+
19
+ # process field metadata
20
+ root.elements['metadata'].each_element('field-definition') { |field|
21
+ name = field.attributes['name']
22
+ @fields[name] = Field.new(field)
23
+ }
24
+ @fields.freeze
25
+
26
+ # process relatedset metadata
27
+ root.elements['metadata'].each_element('relatedset-definition') { |relatedset|
28
+ table = relatedset.attributes['table']
29
+ fields = {}
30
+ relatedset.each_element('field-definition') { |field|
31
+ name = field.attributes['name'].sub(Regexp.new(table + '::'), '')
32
+ fields[name] = Field.new(field)
33
+ }
34
+ @portals[table] = fields
35
+ }
36
+ @portals.freeze
37
+
38
+ # build rows
39
+ root.elements['resultset'].each_element('record') { |record|
40
+ self << Record.new(record, self, @fields, @layout)
41
+ }
42
+ end
43
+
44
+ attr_reader :fields, :portals
45
+
46
+ end
47
+
48
+ class Record < Hash
49
+ def initialize(row_element, resultset, fields, layout, portal=nil)
50
+ @record_id = row_element.attributes['record-id']
51
+ @mod_id = row_element.attributes['mod-id']
52
+ @mods = {}
53
+ @resultset = resultset
54
+ @layout = layout
55
+
56
+ @loaded = false
57
+
58
+ row_element.each_element('field') { |field|
59
+ field_name = field.attributes['name']
60
+ field_name.sub!(Regexp.new(portal + '::'), '') if portal
61
+ datum = []
62
+ field.each_element('data') {|x| datum.push(fields[field_name].coerce(x.text))}
63
+ if datum.length == 1
64
+ self[field_name] = datum[0]
65
+ elsif datum.length == 0
66
+ self[field_name] = nil
67
+ else
68
+ self[field_name] = datum
69
+ end
70
+ }
71
+
72
+ @portals = {}
73
+ row_element.each_element('relatedset') { |relatedset|
74
+ table = relatedset.attributes['table']
75
+ records = []
76
+ relatedset.each_element('record') { |record|
77
+ records << Record.new(record, @resultset, @resultset.portals[table], @layout, table)
78
+ }
79
+ @portals[table] = records
80
+ }
81
+
82
+ @loaded = true
83
+ end
84
+
85
+ attr_reader :record_id, :mod_id, :portals
86
+
87
+ def save
88
+ self.merge(@layout.edit(self.record_id, @mods)[0]) if @mods.size > 0
89
+ @mods.clear
90
+ end
91
+
92
+ def save_if_not_modified
93
+ self.merge(@layout.edit(@record_id, @mods, {'-modid' => @mod_id})[0]) if @mods.size > 0
94
+ @mods.clear
95
+ end
96
+
97
+ def []=(name, value)
98
+ return super if !@loaded
99
+ if self[name] != nil
100
+ @mods[name] = val
101
+ else
102
+ raise "No such field: #{name}"
103
+ end
104
+ end
105
+
106
+ def method_missing (symbol, *attrs)
107
+ # check for simple getter
108
+ val = self[symbol.to_s]
109
+ return val if val != nil
110
+
111
+ # check for setter
112
+ symbol_name = symbol.to_s
113
+ if symbol_name[-1..-1] == '=' && self.has_key?(symbol_name[0..-2])
114
+ return @mods[symbol_name[0..-2]] = attrs[0]
115
+ end
116
+ super
117
+ end
118
+
119
+ def respond_to?(symbol, include_private = false)
120
+ return true if self[symbol.to_s] != nil
121
+ super
122
+ end
123
+ end
124
+
125
+ class Field
126
+ def initialize(field)
127
+ @name = field.attributes['name']
128
+ @result = field.attributes['result']
129
+ @type = field.attributes['type']
130
+ @max_repeats = field.attributes['max-repeats']
131
+ @global = field.attributes['global']
132
+ end
133
+
134
+ attr_reader :name, :type, :max_repeats, :global
135
+
136
+ def coerce(value)
137
+ return nil if (value == nil || value == '') && @result != "text"
138
+ case @result
139
+ when "text"
140
+ return value
141
+ when "number"
142
+ return BigDecimal.new(value)
143
+ when "date"
144
+ (day, month, year) = /(\d\d)\/(\d\d)\/(\d+)/.match(value)
145
+ return Date.new(year.to_i, month.to_i, day.to_i)
146
+ when "time"
147
+ (hour, min, sec) = /(\d+):(\d+):(\d+)/.match(value)
148
+ return DateTime.civil(-4712, 1, 1, hour.to_i, min.to_i, sec.to_i)
149
+ when "timestamp"
150
+ (month, day, year, hour, min, sec) = /(\d+)\/(\d+)\/(\d+)\s+(\d+):(\d+):(\d+)/.match(value)
151
+ return DateTime.civil(year.to_i, month.to_i, day.to_i, hour.to_i, min.to_i, sec.to_i)
152
+ end
153
+ end
154
+ end
155
+
156
+ end
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.0
3
+ specification_version: 1
4
+ name: rfm
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.1.0
7
+ date: 2006-09-28 00:00:00 -07:00
8
+ summary: A package to access FileMaker Pro databases
9
+ require_paths:
10
+ - lib
11
+ email: gwcoffey on gmail.com
12
+ homepage:
13
+ rubyforge_project:
14
+ description:
15
+ autorequire: rfm
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Geoff Coffey
31
+ files:
32
+ - lib/rfm.rb
33
+ - lib/rfm_command.rb
34
+ - lib/rfm_error.rb
35
+ - lib/rfm_factory.rb
36
+ - lib/rfm_result.rb
37
+ - README
38
+ test_files: []
39
+
40
+ rdoc_options: []
41
+
42
+ extra_rdoc_files:
43
+ - README
44
+ executables: []
45
+
46
+ extensions: []
47
+
48
+ requirements: []
49
+
50
+ dependencies: []
51
+