jschairb-rets4r 1.1.18
Sign up to get free protection for your applications and to get access to all the features.
- 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
|