fap 0.0.1
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/History.txt +4 -0
- data/Manifest.txt +22 -0
- data/README.rdoc +74 -0
- data/Rakefile +16 -0
- data/lib/fap.rb +32 -0
- data/lib/fap/collection.rb +29 -0
- data/lib/fap/fap.rb +41 -0
- data/lib/fap/mixins.rb +6 -0
- data/lib/fap/mixins/properties.rb +77 -0
- data/lib/fap/mixins/relations.rb +85 -0
- data/lib/fap/property.rb +33 -0
- data/lib/fap/relation.rb +15 -0
- data/lib/fap/support/class.rb +157 -0
- data/script/console +11 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/spec/fap_spec.rb +157 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +11 -0
- data/tasks/rspec.rake +21 -0
- metadata +107 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
History.txt
|
|
2
|
+
Manifest.txt
|
|
3
|
+
README.rdoc
|
|
4
|
+
Rakefile
|
|
5
|
+
lib/fap.rb
|
|
6
|
+
lib/fap/relation.rb
|
|
7
|
+
lib/fap/support
|
|
8
|
+
lib/fap/support/class.rb
|
|
9
|
+
lib/fap/fap.rb
|
|
10
|
+
lib/fap/mixins
|
|
11
|
+
lib/fap/mixins/properties.rb
|
|
12
|
+
lib/fap/mixins/relations.rb
|
|
13
|
+
lib/fap/mixins.rb
|
|
14
|
+
lib/fap/collection.rb
|
|
15
|
+
lib/fap/property.rb
|
|
16
|
+
script/console
|
|
17
|
+
script/destroy
|
|
18
|
+
script/generate
|
|
19
|
+
spec/fap_spec.rb
|
|
20
|
+
spec/spec.opts
|
|
21
|
+
spec/spec_helper.rb
|
|
22
|
+
tasks/rspec.rake
|
data/README.rdoc
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
= fap
|
|
2
|
+
|
|
3
|
+
* http://github.com/oz/fap
|
|
4
|
+
|
|
5
|
+
== DESCRIPTION:
|
|
6
|
+
|
|
7
|
+
FAP is a ruby gem build on top of the excellent Nokogiri, to turn boring XML,
|
|
8
|
+
or HTML documents into yummy ruby objects. Right now, it only support
|
|
9
|
+
using Nokogiri's XPath selectors, and simple "relations" between a document
|
|
10
|
+
nodes, though this will hopefully get better.
|
|
11
|
+
|
|
12
|
+
FAP's ideas are loosely connected to tools built by some adventurous fellas at
|
|
13
|
+
AF83, who still do PHP things to their brains. Some credits should go to them,
|
|
14
|
+
and to the horrid weather that kept me locked inside last week-end.
|
|
15
|
+
|
|
16
|
+
And yes, I know it's a stupid name. But I'm sure you can come up with a decent
|
|
17
|
+
acronym. :)
|
|
18
|
+
|
|
19
|
+
== SYNOPSIS:
|
|
20
|
+
|
|
21
|
+
class Atom < FAP::FAP
|
|
22
|
+
string :title, :xpath => '//feed/title'
|
|
23
|
+
uri :url, :xpath => '//feed/link[@rel="self"]', :get => :href
|
|
24
|
+
date :updated, :xpath => '//feed/updated'
|
|
25
|
+
|
|
26
|
+
has_many :articles, :class => 'Article', :xpath => '//feed/entry'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class Article < FAP::FAP
|
|
30
|
+
string :title # It is assumed here, that you want a string mapped on a <title> element
|
|
31
|
+
uri :url, :xpath => 'link[@rel="alternate"]', :get => :href # Fetch the :href attribute
|
|
32
|
+
string :content
|
|
33
|
+
date :updated_at, :xpath => 'updated'
|
|
34
|
+
|
|
35
|
+
belongs_to :atom, :class => 'Atom'
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
atom = Atom.new open("http://search.twitter.com/search.atom?q=ruby")
|
|
39
|
+
atom.articles.each do |article|
|
|
40
|
+
puts "#{article.title}: #{article.url}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
== REQUIREMENTS:
|
|
44
|
+
|
|
45
|
+
* nokogiri
|
|
46
|
+
|
|
47
|
+
== INSTALL:
|
|
48
|
+
|
|
49
|
+
* gem install fap
|
|
50
|
+
|
|
51
|
+
== LICENSE:
|
|
52
|
+
|
|
53
|
+
(The MIT License)
|
|
54
|
+
|
|
55
|
+
Copyright (c) 2010 Arnaud Berthomier
|
|
56
|
+
|
|
57
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
58
|
+
a copy of this software and associated documentation files (the
|
|
59
|
+
'Software'), to deal in the Software without restriction, including
|
|
60
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
61
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
62
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
63
|
+
the following conditions:
|
|
64
|
+
|
|
65
|
+
The above copyright notice and this permission notice shall be
|
|
66
|
+
included in all copies or substantial portions of the Software.
|
|
67
|
+
|
|
68
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
|
69
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
70
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
71
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
72
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
73
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
74
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
gem 'hoe', '>= 2.1.0'
|
|
3
|
+
require 'hoe'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require './lib/fap'
|
|
6
|
+
|
|
7
|
+
Hoe.plugin :newgem
|
|
8
|
+
$hoe = Hoe.spec 'fap' do
|
|
9
|
+
self.developer 'Arnaud Berthomier', 'oz@cyprio.net'
|
|
10
|
+
self.rubyforge_name = self.name # TODO this is default value
|
|
11
|
+
# self.extra_deps = [['activesupport','>= 2.0.2']]
|
|
12
|
+
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
require 'newgem/tasks'
|
|
16
|
+
Dir['tasks/**/*.rake'].each { |t| load t }
|
data/lib/fap.rb
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
|
2
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
|
3
|
+
|
|
4
|
+
require 'nokogiri'
|
|
5
|
+
|
|
6
|
+
module FAP
|
|
7
|
+
VERSION = '0.0.1'
|
|
8
|
+
|
|
9
|
+
autoload :FAP, 'fap/fap'
|
|
10
|
+
autoload :Property, 'fap/property'
|
|
11
|
+
autoload :Relation, 'fap/relation'
|
|
12
|
+
autoload :Mixins, 'fap/mixins'
|
|
13
|
+
autoload :Collection, 'fap/collection'
|
|
14
|
+
|
|
15
|
+
# extracted from Extlib
|
|
16
|
+
#
|
|
17
|
+
# Constantize tries to find a declared constant with the name specified
|
|
18
|
+
# in the string. It raises a NameError when the name is not in CamelCase
|
|
19
|
+
# or is not initialized.
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# "Module".constantize #=> Module
|
|
23
|
+
# "Class".constantize #=> Class
|
|
24
|
+
def self.constantize(camel_cased_word)
|
|
25
|
+
unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ camel_cased_word
|
|
26
|
+
raise NameError, "#{camel_cased_word.inspect} is not a valid constant name!"
|
|
27
|
+
end
|
|
28
|
+
Object.module_eval "::#{$1}", __FILE__, __LINE__
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# *fap* *fap* *fap*
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module FAP
|
|
2
|
+
class Collection
|
|
3
|
+
include Enumerable
|
|
4
|
+
|
|
5
|
+
def initialize relation, node
|
|
6
|
+
@nodes = []
|
|
7
|
+
@fetched = false
|
|
8
|
+
@node = node
|
|
9
|
+
@relation = relation
|
|
10
|
+
@klass = ::FAP.constantize relation.klass
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def each
|
|
14
|
+
fetch_nodes unless @fetched
|
|
15
|
+
@nodes.map do |node|
|
|
16
|
+
obj = @klass.new nil
|
|
17
|
+
obj.from_relation @relation, node
|
|
18
|
+
yield obj
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def fetch_nodes
|
|
25
|
+
@nodes = @node.xpath @relation.xpath
|
|
26
|
+
@fetched = true
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/fap/fap.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
module FAP
|
|
4
|
+
class FAP
|
|
5
|
+
|
|
6
|
+
def self.inherited subclass
|
|
7
|
+
subclass.send :include, ::FAP::Mixins::Properties
|
|
8
|
+
subclass.send :include, ::FAP::Mixins::Relations
|
|
9
|
+
subclass.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
|
10
|
+
def self.inherited subclass
|
|
11
|
+
super
|
|
12
|
+
subclass.properties = self.properties.dup
|
|
13
|
+
subclass.relations = self.relations.dup
|
|
14
|
+
end
|
|
15
|
+
EOS
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize stream, opts={}
|
|
19
|
+
data = stream.respond_to?(:read) ? stream.read : stream.to_s
|
|
20
|
+
@_node = Nokogiri data
|
|
21
|
+
@_cache = {}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
##
|
|
25
|
+
# Build object starting from a relation's node
|
|
26
|
+
#
|
|
27
|
+
# @see FAP::Collection
|
|
28
|
+
# @param [FAP::Relation] Object relation
|
|
29
|
+
# @param [Nokogiri::XML::Element] starting node
|
|
30
|
+
# @retun [FAP::FAP]
|
|
31
|
+
def from_relation relation, node
|
|
32
|
+
relation.from.class
|
|
33
|
+
owners = self.relations.select { |rel| rel.type == :belongs_to && rel.klass == relation.from.class.to_s }
|
|
34
|
+
@_cache ||= {}
|
|
35
|
+
@_cache[owners.first.name] = relation.from if owners.size == 1
|
|
36
|
+
@_node = node
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/fap/mixins.rb
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# For extlib_inheritable_accessor
|
|
2
|
+
require File.join File.dirname(__FILE__), '..', 'support', 'class'
|
|
3
|
+
|
|
4
|
+
module FAP
|
|
5
|
+
module Mixins
|
|
6
|
+
module Properties
|
|
7
|
+
|
|
8
|
+
def self.included base
|
|
9
|
+
base.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
|
10
|
+
extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties)
|
|
11
|
+
self.properties ||= []
|
|
12
|
+
EOS
|
|
13
|
+
base.extend ClassMethods
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def _search_property name
|
|
17
|
+
property = properties.select { |prop| prop.name == name }.first
|
|
18
|
+
node = @_node.at_xpath property.xpath
|
|
19
|
+
raise StandardError, "Could not find #{self.class}.#{name} (#{property.xpath}) in stream." unless node
|
|
20
|
+
property.cast node
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module ClassMethods
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
# Define a new property.
|
|
27
|
+
#
|
|
28
|
+
# property :foo, "String", :some => "options"
|
|
29
|
+
# property :foo
|
|
30
|
+
# property :foo, :some => "options"
|
|
31
|
+
#
|
|
32
|
+
# @param [Symbol] property name
|
|
33
|
+
# @param [Array] splat args
|
|
34
|
+
def property name, *args
|
|
35
|
+
opts = {}
|
|
36
|
+
opts.merge!(:type => args.shift) if args[0].class == String
|
|
37
|
+
opts.merge!(*args) unless args.empty?
|
|
38
|
+
property = ::FAP::Property.new name, opts
|
|
39
|
+
self.properties << property
|
|
40
|
+
define_property_getter property
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def string name, *args
|
|
44
|
+
property name, *args
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def number name, *args
|
|
48
|
+
property name, 'Fixnum', *args
|
|
49
|
+
end
|
|
50
|
+
alias :integer :number
|
|
51
|
+
|
|
52
|
+
def date name, *args
|
|
53
|
+
property name, 'DateTime', *args
|
|
54
|
+
end
|
|
55
|
+
alias :time :date
|
|
56
|
+
|
|
57
|
+
def uri name, *args
|
|
58
|
+
property name, 'URI', *args
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
protected
|
|
62
|
+
|
|
63
|
+
def define_property_getter property
|
|
64
|
+
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
|
65
|
+
def #{property.name}
|
|
66
|
+
if @_cache[:#{property.name}]
|
|
67
|
+
@_cache[:#{property.name}]
|
|
68
|
+
else
|
|
69
|
+
@_cache[:#{property.name}] = _search_property :#{property.name}
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
EOS
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# For extlib_inheritable_accessor
|
|
2
|
+
require File.join File.dirname(__FILE__), '..', 'support', 'class'
|
|
3
|
+
|
|
4
|
+
module FAP
|
|
5
|
+
module Mixins
|
|
6
|
+
module Relations
|
|
7
|
+
|
|
8
|
+
def self.included base
|
|
9
|
+
base.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
|
10
|
+
extlib_inheritable_accessor(:relations) unless self.respond_to?(:relations)
|
|
11
|
+
self.relations ||= []
|
|
12
|
+
EOS
|
|
13
|
+
base.extend ClassMethods
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
##
|
|
17
|
+
# Load a relation by its nane
|
|
18
|
+
#
|
|
19
|
+
# @return [FAP::Collection] for "has_many" relations
|
|
20
|
+
# @return [FAP::FAP] "father" object for "belongs_to" relations
|
|
21
|
+
def _load_relation name
|
|
22
|
+
relation = self.relations.select { |rel| rel.name == name }.first
|
|
23
|
+
if relation.type == :has_many
|
|
24
|
+
_load_has_many relation
|
|
25
|
+
elsif relation.type == :belongs_to
|
|
26
|
+
_load_belongs_to relation
|
|
27
|
+
else
|
|
28
|
+
raise "Unkown relation type"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def _load_has_many relation
|
|
33
|
+
relation.from = self
|
|
34
|
+
::FAP::Collection.new relation, @_node
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def _load_belongs_to relation
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
module ClassMethods
|
|
42
|
+
|
|
43
|
+
##
|
|
44
|
+
# Define a "many" relation
|
|
45
|
+
#
|
|
46
|
+
def has_many name, opts={}
|
|
47
|
+
opts.merge! :type => :has_many, :from => self
|
|
48
|
+
relation = ::FAP::Relation.new name, opts
|
|
49
|
+
self.relations << relation
|
|
50
|
+
define_relation_getter relation
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
##
|
|
54
|
+
# Define a "belongs to" relation
|
|
55
|
+
#
|
|
56
|
+
def belongs_to name, opts={}
|
|
57
|
+
opts.merge! :type => :belongs_to, :from => self
|
|
58
|
+
relation = ::FAP::Relation.new name, opts
|
|
59
|
+
self.relations << relation
|
|
60
|
+
define_relation_getter relation
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
protected
|
|
64
|
+
|
|
65
|
+
##
|
|
66
|
+
# Add getter for a relation.
|
|
67
|
+
#
|
|
68
|
+
# @param [FAP::Relation]
|
|
69
|
+
# @return TBD
|
|
70
|
+
def define_relation_getter relation
|
|
71
|
+
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
|
72
|
+
def #{relation.name}
|
|
73
|
+
if @_cache[:#{relation.name}]
|
|
74
|
+
@_cache[:#{relation.name}]
|
|
75
|
+
else
|
|
76
|
+
@_cache[:#{relation.name}] = _load_relation :#{relation.name}
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
EOS
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/fap/property.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module FAP
|
|
2
|
+
class Property
|
|
3
|
+
attr_reader :name, :type, :xpath, :attribute
|
|
4
|
+
|
|
5
|
+
def initialize name, opt={}
|
|
6
|
+
@name = name
|
|
7
|
+
@type = opt[:type] || 'String'
|
|
8
|
+
@xpath = opt[:xpath] || name.to_s
|
|
9
|
+
@attribute = opt[:get] || nil
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
##
|
|
13
|
+
# Cast a Nokogiri node value to @type
|
|
14
|
+
# @param [Nokogiri::XML::Element] a node
|
|
15
|
+
# @return value of type @type
|
|
16
|
+
def cast node
|
|
17
|
+
raise "Invalid XML node" if node.nil?
|
|
18
|
+
what = @attribute ? node[@attribute.to_s] : node.text
|
|
19
|
+
case @type
|
|
20
|
+
when 'Fixnum'
|
|
21
|
+
what.to_i 10
|
|
22
|
+
when 'DateTime'
|
|
23
|
+
DateTime.parse what
|
|
24
|
+
when 'URI'
|
|
25
|
+
URI.parse what
|
|
26
|
+
when 'String'
|
|
27
|
+
what.to_s
|
|
28
|
+
else
|
|
29
|
+
::FAP.constantize(@type).new(what)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/fap/relation.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module FAP
|
|
2
|
+
class Relation
|
|
3
|
+
attr_reader :name, :type, :xpath, :klass
|
|
4
|
+
attr_accessor :from
|
|
5
|
+
|
|
6
|
+
def initialize name, opt={}
|
|
7
|
+
@name = name
|
|
8
|
+
@type = opt[:type]
|
|
9
|
+
@xpath = opt[:xpath] || name
|
|
10
|
+
@klass = opt[:class] || nil
|
|
11
|
+
@from = opt[:from] || nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Copyright (c) 2006-2009 David Heinemeier Hansson
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
# a copy of this software and associated documentation files (the
|
|
5
|
+
# "Software"), to deal in the Software without restriction, including
|
|
6
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
# the following conditions:
|
|
10
|
+
#
|
|
11
|
+
# The above copyright notice and this permission notice shall be
|
|
12
|
+
# included in all copies or substantial portions of the Software.
|
|
13
|
+
#
|
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
21
|
+
#
|
|
22
|
+
# Extracted From
|
|
23
|
+
# http://github.com/rails/rails/commit/971e2438d98326c994ec6d3ef8e37b7e868ed6e2
|
|
24
|
+
|
|
25
|
+
# Extends the class object with class and instance accessors for class attributes,
|
|
26
|
+
# just like the native attr* accessors for instance attributes.
|
|
27
|
+
#
|
|
28
|
+
# class Person
|
|
29
|
+
# cattr_accessor :hair_colors
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# Person.hair_colors = [:brown, :black, :blonde, :red]
|
|
33
|
+
class Class
|
|
34
|
+
def cattr_reader(*syms)
|
|
35
|
+
syms.flatten.each do |sym|
|
|
36
|
+
next if sym.is_a?(Hash)
|
|
37
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
|
38
|
+
unless defined? @@#{sym} # unless defined? @@hair_colors
|
|
39
|
+
@@#{sym} = nil # @@hair_colors = nil
|
|
40
|
+
end # end
|
|
41
|
+
#
|
|
42
|
+
def self.#{sym} # def self.hair_colors
|
|
43
|
+
@@#{sym} # @@hair_colors
|
|
44
|
+
end # end
|
|
45
|
+
#
|
|
46
|
+
def #{sym} # def hair_colors
|
|
47
|
+
@@#{sym} # @@hair_colors
|
|
48
|
+
end # end
|
|
49
|
+
EOS
|
|
50
|
+
end
|
|
51
|
+
end unless Class.respond_to?(:cattr_reader)
|
|
52
|
+
|
|
53
|
+
def cattr_writer(*syms)
|
|
54
|
+
options = syms.extract_options!
|
|
55
|
+
syms.flatten.each do |sym|
|
|
56
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
|
57
|
+
unless defined? @@#{sym} # unless defined? @@hair_colors
|
|
58
|
+
@@#{sym} = nil # @@hair_colors = nil
|
|
59
|
+
end # end
|
|
60
|
+
#
|
|
61
|
+
def self.#{sym}=(obj) # def self.hair_colors=(obj)
|
|
62
|
+
@@#{sym} = obj # @@hair_colors = obj
|
|
63
|
+
end # end
|
|
64
|
+
#
|
|
65
|
+
#{" #
|
|
66
|
+
def #{sym}=(obj) # def hair_colors=(obj)
|
|
67
|
+
@@#{sym} = obj # @@hair_colors = obj
|
|
68
|
+
end # end
|
|
69
|
+
" unless options[:instance_writer] == false } # # instance writer above is generated unless options[:instance_writer] == false
|
|
70
|
+
EOS
|
|
71
|
+
end
|
|
72
|
+
end unless Class.respond_to?(:cattr_writer)
|
|
73
|
+
|
|
74
|
+
def cattr_accessor(*syms)
|
|
75
|
+
cattr_reader(*syms)
|
|
76
|
+
cattr_writer(*syms)
|
|
77
|
+
end unless Class.respond_to?(:cattr_accessor)
|
|
78
|
+
|
|
79
|
+
# Defines class-level inheritable attribute reader. Attributes are available to subclasses,
|
|
80
|
+
# each subclass has a copy of parent's attribute.
|
|
81
|
+
#
|
|
82
|
+
# @param *syms<Array[#to_s]> Array of attributes to define inheritable reader for.
|
|
83
|
+
# @return <Array[#to_s]> Array of attributes converted into inheritable_readers.
|
|
84
|
+
#
|
|
85
|
+
# @api public
|
|
86
|
+
#
|
|
87
|
+
# @todo Do we want to block instance_reader via :instance_reader => false
|
|
88
|
+
# @todo It would be preferable that we do something with a Hash passed in
|
|
89
|
+
# (error out or do the same as other methods above) instead of silently
|
|
90
|
+
# moving on). In particular, this makes the return value of this function
|
|
91
|
+
# less useful.
|
|
92
|
+
def extlib_inheritable_reader(*ivars)
|
|
93
|
+
instance_reader = ivars.pop[:reader] if ivars.last.is_a?(Hash)
|
|
94
|
+
|
|
95
|
+
ivars.each do |ivar|
|
|
96
|
+
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
97
|
+
def self.#{ivar}
|
|
98
|
+
return @#{ivar} if self.object_id == #{self.object_id} || defined?(@#{ivar})
|
|
99
|
+
ivar = superclass.#{ivar}
|
|
100
|
+
return nil if ivar.nil? && !#{self}.instance_variable_defined?("@#{ivar}")
|
|
101
|
+
@#{ivar} = ivar && !ivar.is_a?(Module) && !ivar.is_a?(Numeric) && !ivar.is_a?(TrueClass) && !ivar.is_a?(FalseClass) ? ivar.dup : ivar
|
|
102
|
+
end
|
|
103
|
+
RUBY
|
|
104
|
+
unless instance_reader == false
|
|
105
|
+
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
106
|
+
def #{ivar}
|
|
107
|
+
self.class.#{ivar}
|
|
108
|
+
end
|
|
109
|
+
RUBY
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end unless Class.respond_to?(:extlib_inheritable_reader)
|
|
113
|
+
|
|
114
|
+
# Defines class-level inheritable attribute writer. Attributes are available to subclasses,
|
|
115
|
+
# each subclass has a copy of parent's attribute.
|
|
116
|
+
#
|
|
117
|
+
# @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to
|
|
118
|
+
# define inheritable writer for.
|
|
119
|
+
# @option syms :instance_writer<Boolean> if true, instance-level inheritable attribute writer is defined.
|
|
120
|
+
# @return <Array[#to_s]> An Array of the attributes that were made into inheritable writers.
|
|
121
|
+
#
|
|
122
|
+
# @api public
|
|
123
|
+
#
|
|
124
|
+
# @todo We need a style for class_eval <<-HEREDOC. I'd like to make it
|
|
125
|
+
# class_eval(<<-RUBY, __FILE__, __LINE__), but we should codify it somewhere.
|
|
126
|
+
def extlib_inheritable_writer(*ivars)
|
|
127
|
+
instance_writer = ivars.pop[:writer] if ivars.last.is_a?(Hash)
|
|
128
|
+
ivars.each do |ivar|
|
|
129
|
+
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
130
|
+
def self.#{ivar}=(obj)
|
|
131
|
+
@#{ivar} = obj
|
|
132
|
+
end
|
|
133
|
+
RUBY
|
|
134
|
+
unless instance_writer == false
|
|
135
|
+
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
136
|
+
def #{ivar}=(obj) self.class.#{ivar} = obj end
|
|
137
|
+
RUBY
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
self.send("#{ivar}=", yield) if block_given?
|
|
141
|
+
end
|
|
142
|
+
end unless Class.respond_to?(:extlib_inheritable_writer)
|
|
143
|
+
|
|
144
|
+
# Defines class-level inheritable attribute accessor. Attributes are available to subclasses,
|
|
145
|
+
# each subclass has a copy of parent's attribute.
|
|
146
|
+
#
|
|
147
|
+
# @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to
|
|
148
|
+
# define inheritable accessor for.
|
|
149
|
+
# @option syms :instance_writer<Boolean> if true, instance-level inheritable attribute writer is defined.
|
|
150
|
+
# @return <Array[#to_s]> An Array of attributes turned into inheritable accessors.
|
|
151
|
+
#
|
|
152
|
+
# @api public
|
|
153
|
+
def extlib_inheritable_accessor(*syms, &block)
|
|
154
|
+
extlib_inheritable_reader(*syms)
|
|
155
|
+
extlib_inheritable_writer(*syms, &block)
|
|
156
|
+
end unless Class.respond_to?(:extlib_inheritable_accessor)
|
|
157
|
+
end
|
data/script/console
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# File: script/console
|
|
3
|
+
irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
|
|
4
|
+
|
|
5
|
+
libs = " -r irb/completion"
|
|
6
|
+
libs << " -r rubygems" unless ENV['NO_RUBYGEMS']
|
|
7
|
+
# Perhaps use a console_lib to store any extra methods I may want available in the cosole
|
|
8
|
+
# libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
|
|
9
|
+
libs << " -r #{File.dirname(__FILE__) + '/../lib/fap.rb'}"
|
|
10
|
+
puts "Loading fap gem"
|
|
11
|
+
exec "#{irb} #{libs} --simple-prompt"
|
data/script/destroy
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
|
3
|
+
|
|
4
|
+
begin
|
|
5
|
+
require 'rubigen'
|
|
6
|
+
rescue LoadError
|
|
7
|
+
require 'rubygems'
|
|
8
|
+
require 'rubigen'
|
|
9
|
+
end
|
|
10
|
+
require 'rubigen/scripts/destroy'
|
|
11
|
+
|
|
12
|
+
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
|
|
13
|
+
RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
|
|
14
|
+
RubiGen::Scripts::Destroy.new.run(ARGV)
|
data/script/generate
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
|
3
|
+
|
|
4
|
+
begin
|
|
5
|
+
require 'rubigen'
|
|
6
|
+
rescue LoadError
|
|
7
|
+
require 'rubygems'
|
|
8
|
+
require 'rubigen'
|
|
9
|
+
end
|
|
10
|
+
require 'rubigen/scripts/generate'
|
|
11
|
+
|
|
12
|
+
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
|
|
13
|
+
RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
|
|
14
|
+
RubiGen::Scripts::Generate.new.run(ARGV)
|
data/spec/fap_spec.rb
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
|
2
|
+
|
|
3
|
+
describe "a very light FAP" do
|
|
4
|
+
before :all do
|
|
5
|
+
class Foo < FAP::FAP ; end
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
it "should define #properties #accessor" do
|
|
9
|
+
Foo.properties.should be_an_instance_of(Array)
|
|
10
|
+
Foo.properties.should be_empty
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "should define #relations read accessor" do
|
|
14
|
+
Foo.relations.should be_an_instance_of(Array)
|
|
15
|
+
Foo.relations.should be_empty
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe "a very light FAP with properties" do
|
|
20
|
+
before :all do
|
|
21
|
+
class Foo < FAP::FAP
|
|
22
|
+
property :str_name
|
|
23
|
+
property :str_prop, 'String'
|
|
24
|
+
property :xpath_prop, 'Fixnum', :xpath => '//foobar'
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "should have 3 properties" do
|
|
29
|
+
Foo.properties.should have(3).items
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "should have 2 'String' properties" do
|
|
33
|
+
props = Foo.properties.select { |prop| prop.name != :xpath_prop }
|
|
34
|
+
props.each do |prop|
|
|
35
|
+
prop.type.should == 'String'
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "should have 1 'Fixnum' property" do
|
|
40
|
+
props = Foo.properties.select { |prop| prop.name == :xpath_prop }
|
|
41
|
+
props.each do |prop|
|
|
42
|
+
prop.type.should == 'Fixnum'
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "should define a property's xpath with the :xpath option" do
|
|
47
|
+
props = Foo.properties.select { |prop| prop.name == :xpath_prop }
|
|
48
|
+
props.each do |prop|
|
|
49
|
+
prop.xpath.should == '//foobar'
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "should define property accessors" do
|
|
54
|
+
x = Foo.new nil
|
|
55
|
+
x.should respond_to(:str_name)
|
|
56
|
+
x.should respond_to(:str_prop)
|
|
57
|
+
x.should respond_to(:xpath_prop)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
describe "a FAP with some relations" do
|
|
62
|
+
before :all do
|
|
63
|
+
class Foo < FAP::FAP
|
|
64
|
+
has_many :bars, :class => 'Bar', :xpath => '//foo/bars'
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class Bar < FAP::FAP
|
|
68
|
+
belongs_to :foo, :class => 'Foo'
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "should define some relations" do
|
|
73
|
+
Foo.relations.should have(1).item
|
|
74
|
+
Bar.relations.should have(1).item
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "should define has_many relations" do
|
|
78
|
+
x = Foo.new nil
|
|
79
|
+
x.should respond_to(:bars)
|
|
80
|
+
x.bars.should be_an_instance_of(FAP::Collection)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "should define belongs_to relations" do
|
|
84
|
+
x = Bar.new nil
|
|
85
|
+
x.should respond_to(:foo)
|
|
86
|
+
x.foo.should be_an_instance_of(NilClass)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
describe "a simple FAP to read a stream" do
|
|
91
|
+
before :all do
|
|
92
|
+
class Foo < FAP::FAP ; end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "should load a File object" do
|
|
96
|
+
foo = Foo.new open('spec/fixtures/twitter-search.atom')
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe "a simple FAP to parse an atom feed" do
|
|
101
|
+
before :all do
|
|
102
|
+
# Define a simple Atom parser for twitter searches.
|
|
103
|
+
class Atom < FAP::FAP
|
|
104
|
+
string :title, :xpath => '//feed/title'
|
|
105
|
+
uri :url, :xpath => '//feed/link[@rel="self"]', :get => :href
|
|
106
|
+
date :updated, :xpath => '//feed/updated'
|
|
107
|
+
|
|
108
|
+
has_many :articles, :class => 'Article', :xpath => '//feed/entry'
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
class Article < FAP::FAP
|
|
112
|
+
string :title
|
|
113
|
+
uri :url, :xpath => 'link[@rel="alternate"]', :get => :href
|
|
114
|
+
string :content
|
|
115
|
+
date :updated_at, :xpath => 'updated'
|
|
116
|
+
|
|
117
|
+
belongs_to :atom, :class => 'Atom'
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
before :each do
|
|
122
|
+
@atom = Atom.new open('spec/fixtures/twitter-search.atom')
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "should find a title" do
|
|
126
|
+
@atom.title.should == "ruby - Twitter Search"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "should find a URI" do
|
|
130
|
+
@atom.url.should be_an_instance_of(URI::HTTP)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it "should find a date" do
|
|
134
|
+
@atom.updated.should be_an_instance_of(DateTime)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it "should define a collection of :articles" do
|
|
138
|
+
@atom.articles.should be_an_instance_of(FAP::Collection)
|
|
139
|
+
@atom.articles.should respond_to(:each)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it "should map relations to their required class" do
|
|
143
|
+
@atom.articles.first.should be_an_instance_of(Article)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it "should map relations to their required class instances" do
|
|
147
|
+
@atom.articles.first.title.should be_an_instance_of(String)
|
|
148
|
+
@atom.articles.first.url.should be_an_instance_of(URI::HTTP)
|
|
149
|
+
@atom.articles.first.content.should be_an_instance_of(String)
|
|
150
|
+
@atom.articles.first.updated_at.should be_an_instance_of(DateTime)
|
|
151
|
+
@atom.articles.first.atom.should be_an_instance_of(Atom)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it "should preserve relations' objects" do
|
|
155
|
+
@atom.articles.first.atom.object_id.should == @atom.object_id
|
|
156
|
+
end
|
|
157
|
+
end
|
data/spec/spec.opts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--colour
|
data/spec/spec_helper.rb
ADDED
data/tasks/rspec.rake
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
begin
|
|
2
|
+
require 'spec'
|
|
3
|
+
rescue LoadError
|
|
4
|
+
require 'rubygems' unless ENV['NO_RUBYGEMS']
|
|
5
|
+
require 'spec'
|
|
6
|
+
end
|
|
7
|
+
begin
|
|
8
|
+
require 'spec/rake/spectask'
|
|
9
|
+
rescue LoadError
|
|
10
|
+
puts <<-EOS
|
|
11
|
+
To use rspec for testing you must install rspec gem:
|
|
12
|
+
gem install rspec
|
|
13
|
+
EOS
|
|
14
|
+
exit(0)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
desc "Run the specs under spec/models"
|
|
18
|
+
Spec::Rake::SpecTask.new do |t|
|
|
19
|
+
t.spec_opts = ['--options', "spec/spec.opts"]
|
|
20
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
|
21
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: fap
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Arnaud Berthomier
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
|
|
12
|
+
date: 2010-03-31 00:00:00 +02:00
|
|
13
|
+
default_executable:
|
|
14
|
+
dependencies:
|
|
15
|
+
- !ruby/object:Gem::Dependency
|
|
16
|
+
name: rubyforge
|
|
17
|
+
type: :development
|
|
18
|
+
version_requirement:
|
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
20
|
+
requirements:
|
|
21
|
+
- - ">="
|
|
22
|
+
- !ruby/object:Gem::Version
|
|
23
|
+
version: 2.0.4
|
|
24
|
+
version:
|
|
25
|
+
- !ruby/object:Gem::Dependency
|
|
26
|
+
name: hoe
|
|
27
|
+
type: :development
|
|
28
|
+
version_requirement:
|
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 2.6.0
|
|
34
|
+
version:
|
|
35
|
+
description: |-
|
|
36
|
+
FAP is a ruby gem build on top of the excellent Nokogiri, to turn boring XML,
|
|
37
|
+
or HTML documents into yummy ruby objects. Right now, it only support
|
|
38
|
+
using Nokogiri's XPath selectors, and simple "relations" between a document
|
|
39
|
+
nodes, though this will hopefully get better.
|
|
40
|
+
|
|
41
|
+
FAP's ideas are loosely connected to tools built by some adventurous fellas at
|
|
42
|
+
AF83, who still do PHP things to their brains. Some credits should go to them,
|
|
43
|
+
and to the horrid weather that kept me locked inside last week-end.
|
|
44
|
+
|
|
45
|
+
And yes, I know it's a stupid name. But I'm sure you can come up with a decent
|
|
46
|
+
acronym. :)
|
|
47
|
+
email:
|
|
48
|
+
- oz@cyprio.net
|
|
49
|
+
executables: []
|
|
50
|
+
|
|
51
|
+
extensions: []
|
|
52
|
+
|
|
53
|
+
extra_rdoc_files:
|
|
54
|
+
- History.txt
|
|
55
|
+
- Manifest.txt
|
|
56
|
+
files:
|
|
57
|
+
- History.txt
|
|
58
|
+
- Manifest.txt
|
|
59
|
+
- README.rdoc
|
|
60
|
+
- Rakefile
|
|
61
|
+
- lib/fap.rb
|
|
62
|
+
- lib/fap/relation.rb
|
|
63
|
+
- lib/fap/support/class.rb
|
|
64
|
+
- lib/fap/fap.rb
|
|
65
|
+
- lib/fap/mixins/properties.rb
|
|
66
|
+
- lib/fap/mixins/relations.rb
|
|
67
|
+
- lib/fap/mixins.rb
|
|
68
|
+
- lib/fap/collection.rb
|
|
69
|
+
- lib/fap/property.rb
|
|
70
|
+
- script/console
|
|
71
|
+
- script/destroy
|
|
72
|
+
- script/generate
|
|
73
|
+
- spec/fap_spec.rb
|
|
74
|
+
- spec/spec.opts
|
|
75
|
+
- spec/spec_helper.rb
|
|
76
|
+
- tasks/rspec.rake
|
|
77
|
+
has_rdoc: true
|
|
78
|
+
homepage: http://github.com/oz/fap
|
|
79
|
+
licenses: []
|
|
80
|
+
|
|
81
|
+
post_install_message:
|
|
82
|
+
rdoc_options:
|
|
83
|
+
- --main
|
|
84
|
+
- README.rdoc
|
|
85
|
+
require_paths:
|
|
86
|
+
- lib
|
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
88
|
+
requirements:
|
|
89
|
+
- - ">="
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: "0"
|
|
92
|
+
version:
|
|
93
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
94
|
+
requirements:
|
|
95
|
+
- - ">="
|
|
96
|
+
- !ruby/object:Gem::Version
|
|
97
|
+
version: "0"
|
|
98
|
+
version:
|
|
99
|
+
requirements: []
|
|
100
|
+
|
|
101
|
+
rubyforge_project: fap
|
|
102
|
+
rubygems_version: 1.3.5
|
|
103
|
+
signing_key:
|
|
104
|
+
specification_version: 3
|
|
105
|
+
summary: FAP is a ruby gem build on top of the excellent Nokogiri, to turn boring XML, or HTML documents into yummy ruby objects
|
|
106
|
+
test_files: []
|
|
107
|
+
|