fap 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ === 0.0.1 2010-03-30
2
+
3
+ * 1 major enhancement:
4
+ * Initial release, yay.
@@ -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
@@ -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.
@@ -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 }
@@ -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
@@ -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
@@ -0,0 +1,6 @@
1
+ module FAP
2
+ module Mixins
3
+ autoload :Properties, 'fap/mixins/properties'
4
+ autoload :Relations, 'fap/mixins/relations'
5
+ end
6
+ end
@@ -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
@@ -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
@@ -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
@@ -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"
@@ -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)
@@ -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)
@@ -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
@@ -0,0 +1 @@
1
+ --colour
@@ -0,0 +1,11 @@
1
+ begin
2
+ require 'rubygems' unless ENV['NO_RUBYGEMS']
3
+ require 'spec'
4
+ rescue LoadError
5
+ require 'rubygems' unless ENV['NO_RUBYGEMS']
6
+ gem 'rspec'
7
+ require 'spec'
8
+ end
9
+
10
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
11
+ require 'fap'
@@ -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
+