fap 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|