jschairb-rets4r 1.1.18
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/.document +5 -0
- data/CHANGELOG +566 -0
- data/CONTRIBUTORS +7 -0
- data/Gemfile +10 -0
- data/LICENSE +29 -0
- data/MANIFEST +62 -0
- data/NEWS +186 -0
- data/README.rdoc +43 -0
- data/RUBYS +56 -0
- data/Rakefile +50 -0
- data/TODO +35 -0
- data/examples/client_get_object.rb +49 -0
- data/examples/client_login.rb +39 -0
- data/examples/client_mapper.rb +17 -0
- data/examples/client_metadata.rb +42 -0
- data/examples/client_parser.rb +9 -0
- data/examples/client_search.rb +49 -0
- data/examples/settings.yml +114 -0
- data/lib/rets4r.rb +14 -0
- data/lib/rets4r/auth.rb +73 -0
- data/lib/rets4r/client.rb +487 -0
- data/lib/rets4r/client/data.rb +14 -0
- data/lib/rets4r/client/dataobject.rb +28 -0
- data/lib/rets4r/client/exceptions.rb +116 -0
- data/lib/rets4r/client/links.rb +32 -0
- data/lib/rets4r/client/metadata.rb +15 -0
- data/lib/rets4r/client/parsers/compact.rb +42 -0
- data/lib/rets4r/client/parsers/compact_nokogiri.rb +91 -0
- data/lib/rets4r/client/parsers/metadata.rb +92 -0
- data/lib/rets4r/client/parsers/response_parser.rb +100 -0
- data/lib/rets4r/client/requester.rb +143 -0
- data/lib/rets4r/client/transaction.rb +31 -0
- data/lib/rets4r/core_ext/array/extract_options.rb +15 -0
- data/lib/rets4r/core_ext/class/attribute_accessors.rb +58 -0
- data/lib/rets4r/core_ext/hash/keys.rb +46 -0
- data/lib/rets4r/core_ext/hash/slice.rb +39 -0
- data/lib/rets4r/listing_mapper.rb +17 -0
- data/lib/rets4r/listing_service.rb +35 -0
- data/lib/rets4r/loader.rb +8 -0
- data/lib/tasks/annotations.rake +121 -0
- data/lib/tasks/coverage.rake +13 -0
- data/rets4r.gemspec +24 -0
- data/spec/rets4r_compact_data_parser_spec.rb +7 -0
- data/test/data/1.5/bad_compact.xml +7 -0
- data/test/data/1.5/count_only_compact.xml +3 -0
- data/test/data/1.5/error.xml +1 -0
- data/test/data/1.5/invalid_compact.xml +4 -0
- data/test/data/1.5/login.xml +16 -0
- data/test/data/1.5/metadata.xml +0 -0
- data/test/data/1.5/search_compact.xml +8 -0
- data/test/data/1.5/search_compact_big.xml +136 -0
- data/test/data/1.5/search_unescaped_compact.xml +8 -0
- data/test/data/listing_service.yml +36 -0
- data/test/test_auth.rb +68 -0
- data/test/test_client.rb +342 -0
- data/test/test_client_links.rb +39 -0
- data/test/test_compact_nokogiri.rb +64 -0
- data/test/test_helper.rb +12 -0
- data/test/test_listing_mapper.rb +112 -0
- data/test/test_loader.rb +24 -0
- data/test/test_parser.rb +96 -0
- data/test/test_quality.rb +57 -0
- metadata +211 -0
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'rets4r'
|
2
|
+
require 'rets4r/client/exceptions'
|
3
|
+
|
4
|
+
module RETS4R
|
5
|
+
class Client
|
6
|
+
class Requester
|
7
|
+
DEFAULT_USER_AGENT = "rets4r/#{::RETS4R::VERSION}"
|
8
|
+
DEFAULT_RETS_VERSION = '1.7'
|
9
|
+
|
10
|
+
attr_accessor :logger, :headers, :pre_request_block, :nc, :username, :password, :method
|
11
|
+
def initialize
|
12
|
+
@nc = 0
|
13
|
+
@headers = {
|
14
|
+
'User-Agent' => DEFAULT_USER_AGENT,
|
15
|
+
'Accept' => '*/*',
|
16
|
+
'RETS-Version' => "RETS/#{DEFAULT_RETS_VERSION}",
|
17
|
+
}
|
18
|
+
@pre_request_block = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def user_agent
|
22
|
+
@headers['User-Agent']
|
23
|
+
end
|
24
|
+
|
25
|
+
def user_agent=(name)
|
26
|
+
set_header('User-Agent', name)
|
27
|
+
end
|
28
|
+
|
29
|
+
def rets_version=(version)
|
30
|
+
if (SUPPORTED_RETS_VERSIONS.include? version)
|
31
|
+
set_header('RETS-Version', "RETS/#{version}")
|
32
|
+
else
|
33
|
+
raise Unsupported.new("The client does not support RETS version '#{version}'.")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def rets_version
|
38
|
+
(@headers['RETS-Version'] || "").gsub("RETS/", "")
|
39
|
+
end
|
40
|
+
|
41
|
+
def set_header(name, value)
|
42
|
+
if value.nil? then
|
43
|
+
@headers.delete(name)
|
44
|
+
else
|
45
|
+
@headers[name] = value
|
46
|
+
end
|
47
|
+
|
48
|
+
logger.debug("Set header '#{name}' to '#{value}'") if logger
|
49
|
+
end
|
50
|
+
|
51
|
+
def user_agent
|
52
|
+
@headers['User-Agent']
|
53
|
+
end
|
54
|
+
|
55
|
+
# Given a hash, it returns a URL encoded query string.
|
56
|
+
def create_query_string(hash)
|
57
|
+
parts = hash.map {|key,value| "#{CGI.escape(key)}=#{CGI.escape(value)}" unless key.nil? || value.nil?}
|
58
|
+
return parts.join('&')
|
59
|
+
end
|
60
|
+
# This is the primary transaction method, which the other public methods make use of.
|
61
|
+
# Given a url for the transaction (endpoint) it makes a request to the RETS server.
|
62
|
+
#
|
63
|
+
#--
|
64
|
+
# This needs to be better documented, but for now please see the public transaction methods
|
65
|
+
# for how to make use of this method.
|
66
|
+
#++
|
67
|
+
def request(url, data = {}, header = {}, method = @method, retry_auth = DEFAULT_RETRY)
|
68
|
+
response = ''
|
69
|
+
|
70
|
+
http = Net::HTTP.new(url.host, url.port)
|
71
|
+
http.read_timeout = 600
|
72
|
+
|
73
|
+
if logger && logger.debug?
|
74
|
+
http.set_debug_output HTTPDebugLogger.new(logger)
|
75
|
+
end
|
76
|
+
|
77
|
+
http.start do |http|
|
78
|
+
begin
|
79
|
+
uri = url.path
|
80
|
+
|
81
|
+
if ! data.empty? && method == METHOD_GET
|
82
|
+
uri += "?#{create_query_string(data)}"
|
83
|
+
end
|
84
|
+
|
85
|
+
headers = @headers
|
86
|
+
headers.merge(header) unless header.empty?
|
87
|
+
|
88
|
+
@pre_request_block.call(self, http, headers) if @pre_request_block
|
89
|
+
|
90
|
+
logger.debug(headers.inspect) if logger
|
91
|
+
|
92
|
+
post_data = data.map {|k,v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join('&') if method == METHOD_POST
|
93
|
+
response = method == METHOD_POST ? http.post(uri, post_data, headers) :
|
94
|
+
http.get(uri, headers)
|
95
|
+
|
96
|
+
|
97
|
+
if response.code == '401'
|
98
|
+
# Authentication is required
|
99
|
+
raise AuthRequired
|
100
|
+
elsif response.code.to_i >= 300
|
101
|
+
# We have a non-successful response that we cannot handle
|
102
|
+
raise HTTPError.new(response)
|
103
|
+
else
|
104
|
+
cookies = []
|
105
|
+
if set_cookies = response.get_fields('set-cookie') then
|
106
|
+
set_cookies.each do |cookie|
|
107
|
+
cookies << cookie.split(";").first
|
108
|
+
end
|
109
|
+
end
|
110
|
+
set_header('Cookie', cookies.join("; ")) unless cookies.empty?
|
111
|
+
# totally wrong. session id is only ever under the Cookie header
|
112
|
+
#set_header('RETS-Session-ID', response['RETS-Session-ID']) if response['RETS-Session-ID']
|
113
|
+
set_header('RETS-Session-ID',nil)
|
114
|
+
end
|
115
|
+
rescue AuthRequired
|
116
|
+
@nc += 1
|
117
|
+
|
118
|
+
if retry_auth > 0
|
119
|
+
retry_auth -= 1
|
120
|
+
auth = Auth.authenticate(response,
|
121
|
+
@username,
|
122
|
+
@password,
|
123
|
+
url.path,
|
124
|
+
method,
|
125
|
+
@headers['RETS-Request-ID'],
|
126
|
+
@headers['User-Agent'],
|
127
|
+
@nc)
|
128
|
+
set_header('Authorization', auth)
|
129
|
+
retry
|
130
|
+
else
|
131
|
+
raise LoginError.new(response.message)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
logger.debug(response.body) if logger
|
136
|
+
end
|
137
|
+
|
138
|
+
return response
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module RETS4R
|
2
|
+
class Client
|
3
|
+
class Transaction
|
4
|
+
attr_accessor :reply_code, :reply_text, :response, :metadata,
|
5
|
+
:header, :maxrows, :delimiter, :secondary_response
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
self.maxrows = false
|
9
|
+
self.header = []
|
10
|
+
self.delimiter = ?\t
|
11
|
+
end
|
12
|
+
|
13
|
+
def success?
|
14
|
+
return true if self.reply_code == '0'
|
15
|
+
return false
|
16
|
+
end
|
17
|
+
|
18
|
+
def maxrows?
|
19
|
+
return true if self.maxrows
|
20
|
+
return false
|
21
|
+
end
|
22
|
+
|
23
|
+
def ascii_delimiter
|
24
|
+
self.delimiter.chr
|
25
|
+
end
|
26
|
+
|
27
|
+
# For compatibility with the original library.
|
28
|
+
alias :data :response
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# from ActiveSupport
|
2
|
+
class Array
|
3
|
+
# Extracts options from a set of arguments. Removes and returns the last
|
4
|
+
# element in the array if it's a hash, otherwise returns a blank hash.
|
5
|
+
#
|
6
|
+
# def options(*args)
|
7
|
+
# args.extract_options!
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# options(1, 2) # => {}
|
11
|
+
# options(1, 2, :a => :b) # => {:a=>:b}
|
12
|
+
def extract_options!
|
13
|
+
last.is_a?(::Hash) ? pop : {}
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'rets4r/core_ext/array/extract_options'
|
2
|
+
# from ActiveSupport
|
3
|
+
|
4
|
+
# Extends the class object with class and instance accessors for class attributes,
|
5
|
+
# just like the native attr* accessors for instance attributes.
|
6
|
+
#
|
7
|
+
# class Person
|
8
|
+
# cattr_accessor :hair_colors
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# Person.hair_colors = [:brown, :black, :blonde, :red]
|
12
|
+
class Class
|
13
|
+
def cattr_reader(*syms)
|
14
|
+
syms.flatten.each do |sym|
|
15
|
+
next if sym.is_a?(Hash)
|
16
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
17
|
+
unless defined? @@#{sym} # unless defined? @@hair_colors
|
18
|
+
@@#{sym} = nil # @@hair_colors = nil
|
19
|
+
end # end
|
20
|
+
#
|
21
|
+
def self.#{sym} # def self.hair_colors
|
22
|
+
@@#{sym} # @@hair_colors
|
23
|
+
end # end
|
24
|
+
#
|
25
|
+
def #{sym} # def hair_colors
|
26
|
+
@@#{sym} # @@hair_colors
|
27
|
+
end # end
|
28
|
+
EOS
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def cattr_writer(*syms)
|
33
|
+
options = syms.extract_options!
|
34
|
+
syms.flatten.each do |sym|
|
35
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
36
|
+
unless defined? @@#{sym} # unless defined? @@hair_colors
|
37
|
+
@@#{sym} = nil # @@hair_colors = nil
|
38
|
+
end # end
|
39
|
+
#
|
40
|
+
def self.#{sym}=(obj) # def self.hair_colors=(obj)
|
41
|
+
@@#{sym} = obj # @@hair_colors = obj
|
42
|
+
end # end
|
43
|
+
#
|
44
|
+
#{" #
|
45
|
+
def #{sym}=(obj) # def hair_colors=(obj)
|
46
|
+
@@#{sym} = obj # @@hair_colors = obj
|
47
|
+
end # end
|
48
|
+
" unless options[:instance_writer] == false } # # instance writer above is generated unless options[:instance_writer] == false
|
49
|
+
EOS
|
50
|
+
self.send("#{sym}=", yield) if block_given?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def cattr_accessor(*syms, &blk)
|
55
|
+
cattr_reader(*syms)
|
56
|
+
cattr_writer(*syms, &blk)
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# from ActiveSupport
|
2
|
+
class Hash
|
3
|
+
# Return a new hash with all keys converted to strings.
|
4
|
+
def stringify_keys
|
5
|
+
dup.stringify_keys!
|
6
|
+
end
|
7
|
+
|
8
|
+
# Destructively convert all keys to strings.
|
9
|
+
def stringify_keys!
|
10
|
+
keys.each do |key|
|
11
|
+
self[key.to_s] = delete(key)
|
12
|
+
end
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
# Return a new hash with all keys converted to symbols, as long as
|
17
|
+
# they respond to +to_sym+.
|
18
|
+
def symbolize_keys
|
19
|
+
dup.symbolize_keys!
|
20
|
+
end
|
21
|
+
|
22
|
+
# Destructively convert all keys to symbols, as long as they respond
|
23
|
+
# to +to_sym+.
|
24
|
+
def symbolize_keys!
|
25
|
+
keys.each do |key|
|
26
|
+
self[(key.to_sym rescue key) || key] = delete(key)
|
27
|
+
end
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
alias_method :to_options, :symbolize_keys
|
32
|
+
alias_method :to_options!, :symbolize_keys!
|
33
|
+
|
34
|
+
# Validate all keys in a hash match *valid keys, raising ArgumentError on a mismatch.
|
35
|
+
# Note that keys are NOT treated indifferently, meaning if you use strings for keys but assert symbols
|
36
|
+
# as keys, this will fail.
|
37
|
+
#
|
38
|
+
# ==== Examples
|
39
|
+
# { :name => "Rob", :years => "28" }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key(s): years"
|
40
|
+
# { :name => "Rob", :age => "28" }.assert_valid_keys("name", "age") # => raises "ArgumentError: Unknown key(s): name, age"
|
41
|
+
# { :name => "Rob", :age => "28" }.assert_valid_keys(:name, :age) # => passes, raises nothing
|
42
|
+
def assert_valid_keys(*valid_keys)
|
43
|
+
unknown_keys = keys - [valid_keys].flatten
|
44
|
+
raise(ArgumentError, "Unknown key(s): #{unknown_keys.join(", ")}") unless unknown_keys.empty?
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# from ActiveSupport
|
2
|
+
class Hash
|
3
|
+
# Slice a hash to include only the given keys. This is useful for
|
4
|
+
# limiting an options hash to valid keys before passing to a method:
|
5
|
+
#
|
6
|
+
# def search(criteria = {})
|
7
|
+
# assert_valid_keys(:mass, :velocity, :time)
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# search(options.slice(:mass, :velocity, :time))
|
11
|
+
#
|
12
|
+
# If you have an array of keys you want to limit to, you should splat them:
|
13
|
+
#
|
14
|
+
# valid_keys = [:mass, :velocity, :time]
|
15
|
+
# search(options.slice(*valid_keys))
|
16
|
+
def slice(*keys)
|
17
|
+
keys = keys.map! { |key| convert_key(key) } if respond_to?(:convert_key)
|
18
|
+
hash = self.class.new
|
19
|
+
keys.each { |k| hash[k] = self[k] if has_key?(k) }
|
20
|
+
hash
|
21
|
+
end
|
22
|
+
|
23
|
+
# Replaces the hash with only the given keys.
|
24
|
+
# Returns a hash contained the removed key/value pairs
|
25
|
+
# {:a => 1, :b => 2, :c => 3, :d => 4}.slice!(:a, :b) # => {:c => 3, :d =>4}
|
26
|
+
def slice!(*keys)
|
27
|
+
keys = keys.map! { |key| convert_key(key) } if respond_to?(:convert_key)
|
28
|
+
omit = slice(*self.keys - keys)
|
29
|
+
hash = slice(*keys)
|
30
|
+
replace(hash)
|
31
|
+
omit
|
32
|
+
end
|
33
|
+
|
34
|
+
def extract!(*keys)
|
35
|
+
result = {}
|
36
|
+
keys.each {|key| result[key] = delete(key) }
|
37
|
+
result
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module RETS4R
|
2
|
+
class ListingMapper
|
3
|
+
def initialize(params = {})
|
4
|
+
@select = params[:select] || ListingService.connection[:select]
|
5
|
+
end
|
6
|
+
def select
|
7
|
+
@select
|
8
|
+
end
|
9
|
+
def map(original)
|
10
|
+
listing = {}
|
11
|
+
@select.each_pair {|rets_key, record_key|
|
12
|
+
listing[record_key] = original[rets_key]
|
13
|
+
}
|
14
|
+
listing
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rets4r/core_ext/class/attribute_accessors'
|
2
|
+
require 'rets4r/core_ext/hash/keys'
|
3
|
+
require 'rets4r/core_ext/hash/slice'
|
4
|
+
|
5
|
+
module RETS4R
|
6
|
+
class ListingService
|
7
|
+
# RECORD_COUNT_ONLY=Librets::SearchRequest::RECORD_COUNT_ONLY
|
8
|
+
RECORD_COUNT_ONLY='fixme'
|
9
|
+
# Contains the listing service configurations - typically stored in
|
10
|
+
# config/listing_service.yml - as a Hash.
|
11
|
+
cattr_accessor :configurations, :instance_writer => false
|
12
|
+
cattr_accessor :env, :instance_writer => false
|
13
|
+
|
14
|
+
class << self
|
15
|
+
|
16
|
+
# Connection configuration for the specified environment, or the current
|
17
|
+
# environment if none is given.
|
18
|
+
def connection(spec = nil)
|
19
|
+
case spec
|
20
|
+
when nil
|
21
|
+
connection(RETS4R::ListingService.env)
|
22
|
+
when Symbol, String
|
23
|
+
if configuration = configurations[spec.to_s]
|
24
|
+
configuration.symbolize_keys
|
25
|
+
else
|
26
|
+
raise ArgumentError, "#{spec} listing service is not configured"
|
27
|
+
end
|
28
|
+
else
|
29
|
+
raise ArgumentError, "#{spec} listing service is not configured"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end # class << self
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# Implements the logic behind the rake tasks for annotations like
|
2
|
+
#
|
3
|
+
# rake notes
|
4
|
+
# rake notes:optimize
|
5
|
+
#
|
6
|
+
# and friends. See <tt>rake -T notes</tt> and <tt>railties/lib/tasks/annotations.rake</tt>.
|
7
|
+
#
|
8
|
+
# Annotation objects are triplets <tt>:line</tt>, <tt>:tag</tt>, <tt>:text</tt> that
|
9
|
+
# represent the line where the annotation lives, its tag, and its text. Note
|
10
|
+
# the filename is not stored.
|
11
|
+
#
|
12
|
+
# Annotations are looked for in comments and modulus whitespace they have to
|
13
|
+
# start with the tag optionally followed by a colon. Everything up to the end
|
14
|
+
# of the line (or closing ERb comment tag) is considered to be their text.
|
15
|
+
class SourceAnnotationExtractor
|
16
|
+
class Annotation < Struct.new(:line, :tag, :text)
|
17
|
+
|
18
|
+
# Returns a representation of the annotation that looks like this:
|
19
|
+
#
|
20
|
+
# [126] [TODO] This algorithm is simple and clearly correct, make it faster.
|
21
|
+
#
|
22
|
+
# If +options+ has a flag <tt>:tag</tt> the tag is shown as in the example above.
|
23
|
+
# Otherwise the string contains just line and text.
|
24
|
+
def to_s(options={})
|
25
|
+
s = "[%3d] " % line
|
26
|
+
s << "[#{tag}] " if options[:tag]
|
27
|
+
s << text
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Prints all annotations with tag +tag+ under the root directories +app+, +lib+,
|
32
|
+
# and +test+ (recursively). Only filenames with extension +.builder+, +.rb+,
|
33
|
+
# +.rxml+, +.rjs+, +.rhtml+, or +.erb+ are taken into account. The +options+
|
34
|
+
# hash is passed to each annotation's +to_s+.
|
35
|
+
#
|
36
|
+
# This class method is the single entry point for the rake tasks.
|
37
|
+
def self.enumerate(tag, options={})
|
38
|
+
extractor = new(tag)
|
39
|
+
extractor.display(extractor.find, options)
|
40
|
+
end
|
41
|
+
|
42
|
+
attr_reader :tag
|
43
|
+
|
44
|
+
def initialize(tag)
|
45
|
+
@tag = tag
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns a hash that maps filenames under +dirs+ (recursively) to arrays
|
49
|
+
# with their annotations. Only files with annotations are included, and only
|
50
|
+
# those with extension +.builder+, +.rb+, +.rxml+, +.rjs+, +.rhtml+, and +.erb+
|
51
|
+
# are taken into account.
|
52
|
+
def find(dirs=%w(app lib test))
|
53
|
+
dirs.inject({}) { |h, dir| h.update(find_in(dir)) }
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns a hash that maps filenames under +dir+ (recursively) to arrays
|
57
|
+
# with their annotations. Only files with annotations are included, and only
|
58
|
+
# those with extension +.builder+, +.rb+, +.rxml+, +.rjs+, +.rhtml+, and +.erb+
|
59
|
+
# are taken into account.
|
60
|
+
def find_in(dir)
|
61
|
+
results = {}
|
62
|
+
|
63
|
+
Dir.glob("#{dir}/*") do |item|
|
64
|
+
next if File.basename(item)[0] == ?.
|
65
|
+
|
66
|
+
if File.directory?(item)
|
67
|
+
results.update(find_in(item))
|
68
|
+
elsif item =~ /\.(builder|(r(?:b|xml|js)))$/
|
69
|
+
results.update(extract_annotations_from(item, /#\s*(#{tag}):?\s*(.*)$/))
|
70
|
+
elsif item =~ /\.(rhtml|erb)$/
|
71
|
+
results.update(extract_annotations_from(item, /<%\s*#\s*(#{tag}):?\s*(.*?)\s*%>/))
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
results
|
76
|
+
end
|
77
|
+
|
78
|
+
# If +file+ is the filename of a file that contains annotations this method returns
|
79
|
+
# a hash with a single entry that maps +file+ to an array of its annotations.
|
80
|
+
# Otherwise it returns an empty hash.
|
81
|
+
def extract_annotations_from(file, pattern)
|
82
|
+
lineno = 0
|
83
|
+
result = File.readlines(file).inject([]) do |list, line|
|
84
|
+
lineno += 1
|
85
|
+
next list unless line =~ pattern
|
86
|
+
list << Annotation.new(lineno, $1, $2)
|
87
|
+
end
|
88
|
+
result.empty? ? {} : { file => result }
|
89
|
+
end
|
90
|
+
|
91
|
+
# Prints the mapping from filenames to annotations in +results+ ordered by filename.
|
92
|
+
# The +options+ hash is passed to each annotation's +to_s+.
|
93
|
+
def display(results, options={})
|
94
|
+
results.keys.sort.each do |file|
|
95
|
+
puts "#{file}:"
|
96
|
+
results[file].each do |note|
|
97
|
+
puts " * #{note.to_s(options)}"
|
98
|
+
end
|
99
|
+
puts
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
desc "Enumerate all annotations"
|
105
|
+
task :notes do
|
106
|
+
SourceAnnotationExtractor.enumerate "OPTIMIZE|FIXME|TODO", :tag => true
|
107
|
+
end
|
108
|
+
|
109
|
+
namespace :notes do
|
110
|
+
["OPTIMIZE", "FIXME", "TODO"].each do |annotation|
|
111
|
+
desc "Enumerate all #{annotation} annotations"
|
112
|
+
task annotation.downcase.intern do
|
113
|
+
SourceAnnotationExtractor.enumerate annotation
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
desc "Enumerate a custom annotation, specify with ANNOTATION=WTFHAX"
|
118
|
+
task :custom do
|
119
|
+
SourceAnnotationExtractor.enumerate ENV['ANNOTATION']
|
120
|
+
end
|
121
|
+
end
|