atdis 0.2 → 0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.ruby-version +1 -1
- data/.travis.yml +0 -1
- data/Gemfile +1 -1
- data/README.md +25 -2
- data/Rakefile +1 -1
- data/docs/ATDIS-1.0.2 Application Tracking Data Interchange Specification (v1.0.2).doc +0 -0
- data/docs/ATDIS-1.0.2 Application Tracking Data Interchange Specification (v1.0.2).pdf +0 -0
- data/lib/atdis.rb +2 -7
- data/lib/atdis/feed.rb +80 -8
- data/lib/atdis/model.rb +74 -128
- data/lib/atdis/models/address.rb +17 -0
- data/lib/atdis/models/application.rb +31 -0
- data/lib/atdis/models/authority.rb +19 -0
- data/lib/atdis/models/document.rb +16 -0
- data/lib/atdis/models/event.rb +16 -0
- data/lib/atdis/models/info.rb +81 -0
- data/lib/atdis/models/land_title_ref.rb +26 -0
- data/lib/atdis/models/location.rb +23 -0
- data/lib/atdis/models/page.rb +56 -0
- data/lib/atdis/models/pagination.rb +68 -0
- data/lib/atdis/models/person.rb +14 -0
- data/lib/atdis/models/reference.rb +13 -0
- data/lib/atdis/models/response.rb +16 -0
- data/lib/atdis/models/torrens_title.rb +24 -0
- data/lib/atdis/validators.rb +26 -10
- data/lib/atdis/version.rb +1 -1
- data/spec/atdis/feed_spec.rb +78 -22
- data/spec/atdis/model_spec.rb +80 -131
- data/spec/atdis/models/address_spec.rb +22 -0
- data/spec/atdis/models/application_spec.rb +246 -0
- data/spec/atdis/models/authority_spec.rb +34 -0
- data/spec/atdis/models/document_spec.rb +19 -0
- data/spec/atdis/models/event_spec.rb +29 -0
- data/spec/atdis/models/info_spec.rb +303 -0
- data/spec/atdis/models/land_title_ref_spec.rb +39 -0
- data/spec/atdis/models/location_spec.rb +95 -0
- data/spec/atdis/models/page_spec.rb +296 -0
- data/spec/atdis/models/pagination_spec.rb +153 -0
- data/spec/atdis/models/person_spec.rb +19 -0
- data/spec/atdis/models/reference_spec.rb +55 -0
- data/spec/atdis/models/response_spec.rb +5 -0
- data/spec/atdis/models/torrens_title_spec.rb +52 -0
- data/spec/atdis/separated_url_spec.rb +4 -4
- metadata +141 -135
- data/docs/ATDIS-1.0.7 Application Tracking Data Interchange Specification (v1.0).doc +0 -0
- data/docs/ATDIS-1.0.7 Application Tracking Data Interchange Specification (v1.0).pdf +0 -0
- data/lib/atdis/application.rb +0 -78
- data/lib/atdis/document.rb +0 -14
- data/lib/atdis/event.rb +0 -17
- data/lib/atdis/location.rb +0 -21
- data/lib/atdis/page.rb +0 -130
- data/lib/atdis/person.rb +0 -12
- data/spec/atdis/application_spec.rb +0 -539
- data/spec/atdis/document_spec.rb +0 -19
- data/spec/atdis/event_spec.rb +0 -29
- data/spec/atdis/location_spec.rb +0 -148
- data/spec/atdis/page_spec.rb +0 -492
- data/spec/atdis/person_spec.rb +0 -19
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2b0835df894665d409ea7c29c71f70158e4a4d66
|
4
|
+
data.tar.gz: 07d56dc629ee7916e72388bd881b29952a0b629c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 60c9f8efc76ea97d526acd9f8044878b42baefab92c7ea2939e5987e0a38403b04681a384852d067c584343a60a7209b676702f71fed7bcb205ab1275904244f
|
7
|
+
data.tar.gz: 451d11c16ec89abaa8d6660cd83ba9f91001d97b0f11624b333890eb7c8291bf526cab2784c1c3623d38f2d3d3383ddc29071e17cc5957b887c7692f56c27bb3
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
ruby-
|
1
|
+
ruby-2.0.0-p353
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
@@ -8,7 +8,7 @@ group :development do
|
|
8
8
|
gem 'rb-fsevent', '~> 0.9'
|
9
9
|
# Probably required on OS X. See https://github.com/guard/guard/wiki/Add-Readline-support-to-Ruby-on-Mac-OS-X
|
10
10
|
gem 'rb-readline'
|
11
|
-
gem 'coveralls', :
|
11
|
+
gem 'coveralls', require: false
|
12
12
|
end
|
13
13
|
|
14
14
|
# Specify your gem's dependencies in atdis.gemspec
|
data/README.md
CHANGED
@@ -6,7 +6,7 @@ A ruby interface to the application tracking data interchange specification (ATD
|
|
6
6
|
|
7
7
|
We're developing this against version ATDIS 1.0.7.
|
8
8
|
|
9
|
-
This is **
|
9
|
+
This is **beta** software and is a work in progress.
|
10
10
|
|
11
11
|
Source code is available on GitHub at https://github.com/openaustralia/atdis
|
12
12
|
|
@@ -26,7 +26,30 @@ Or install it yourself as:
|
|
26
26
|
|
27
27
|
## Usage
|
28
28
|
|
29
|
-
|
29
|
+
### Basic usage
|
30
|
+
|
31
|
+
require 'atdis'
|
32
|
+
f = ATDIS::Feed.new("http://www.planningalerts.org.au/atdis/feed/1/atdis/1.0")
|
33
|
+
|
34
|
+
# Get the first application in the first page of results for all the applications
|
35
|
+
page = f.applications
|
36
|
+
app = page.response.first
|
37
|
+
|
38
|
+
puts "#{app.dat_id}: #{app.description} at #{app.location.address}"
|
39
|
+
|
40
|
+
DA2013-0381: New pool plus deck at 123 Fourfivesix Street Neutral Bay NSW 2089
|
41
|
+
|
42
|
+
### Paging
|
43
|
+
|
44
|
+
page.next_page
|
45
|
+
|
46
|
+
and
|
47
|
+
|
48
|
+
page.previous_page
|
49
|
+
|
50
|
+
### Validation
|
51
|
+
|
52
|
+
page.valid?
|
30
53
|
|
31
54
|
## Contributing
|
32
55
|
|
data/Rakefile
CHANGED
Binary file
|
Binary file
|
data/lib/atdis.rb
CHANGED
@@ -1,12 +1,7 @@
|
|
1
1
|
require "atdis/version"
|
2
2
|
|
3
3
|
require "atdis/validators"
|
4
|
-
require "atdis/model"
|
5
|
-
require "atdis/event"
|
6
|
-
require "atdis/document"
|
7
|
-
require "atdis/location"
|
8
|
-
require "atdis/person"
|
9
|
-
require "atdis/application"
|
10
4
|
require "atdis/feed"
|
11
|
-
require "atdis/page"
|
12
5
|
require "atdis/separated_url"
|
6
|
+
require "atdis/model"
|
7
|
+
require "atdis/models/page"
|
data/lib/atdis/feed.rb
CHANGED
@@ -4,18 +4,90 @@ module ATDIS
|
|
4
4
|
class Feed
|
5
5
|
attr_reader :base_url
|
6
6
|
|
7
|
+
VALID_OPTIONS = [:page, :street, :suburb, :postcode, :lodgement_date_start, :lodgement_date_end, :last_modified_date_start, :last_modified_date_end]
|
8
|
+
|
7
9
|
# base_url - the base url from which the urls for all atdis urls are made
|
8
|
-
# It
|
9
|
-
#
|
10
|
-
# applications is "http://www.council.nsw.gov.au/atdis/1.0/applications.json"
|
10
|
+
# It should be of the form:
|
11
|
+
# http://www.council.nsw.gov.au/atdis/1.0
|
11
12
|
def initialize(base_url)
|
12
|
-
@base_url = base_url
|
13
|
+
@base_url = base_url
|
14
|
+
end
|
15
|
+
|
16
|
+
def applications_url(options = {})
|
17
|
+
invalid_options = options.keys - VALID_OPTIONS
|
18
|
+
if !invalid_options.empty?
|
19
|
+
raise "Unexpected options used: #{invalid_options.join(',')}"
|
20
|
+
end
|
21
|
+
options[:street] = options[:street].join(",") if options[:street].respond_to?(:join)
|
22
|
+
options[:suburb] = options[:suburb].join(",") if options[:suburb].respond_to?(:join)
|
23
|
+
options[:postcode] = options[:postcode].join(",") if options[:postcode].respond_to?(:join)
|
24
|
+
|
25
|
+
q = Feed.options_to_query(options)
|
26
|
+
"#{base_url}/applications.json" + (q ? "?#{q}" : "")
|
27
|
+
end
|
28
|
+
|
29
|
+
def application_url(id)
|
30
|
+
"#{base_url}/#{CGI::escape(id)}.json"
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.base_url_from_url(url)
|
34
|
+
u = URI.parse(url)
|
35
|
+
options = query_to_options(u.query)
|
36
|
+
VALID_OPTIONS.each do |o|
|
37
|
+
options.delete(o)
|
38
|
+
end
|
39
|
+
u.query = options_to_query(options)
|
40
|
+
u.fragment = nil
|
41
|
+
u.path = "/" + u.path.split("/")[1..-2].join("/")
|
42
|
+
u.to_s
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.options_from_url(url)
|
46
|
+
u = URI.parse(url)
|
47
|
+
options = query_to_options(u.query)
|
48
|
+
[:lodgement_date_start, :lodgement_date_end, :last_modified_date_start, :last_modified_date_end].each do |k|
|
49
|
+
options[k] = Date.parse(options[k]) if options[k]
|
50
|
+
end
|
51
|
+
options[:page] = options[:page].to_i if options[:page]
|
52
|
+
# Remove invalid options
|
53
|
+
options.keys.each do |key|
|
54
|
+
if !VALID_OPTIONS.include?(key)
|
55
|
+
options.delete(key)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
options
|
59
|
+
end
|
60
|
+
|
61
|
+
def applications(options = {})
|
62
|
+
Models::Page.read_url(applications_url(options))
|
63
|
+
end
|
64
|
+
|
65
|
+
def application(id)
|
66
|
+
Models::Application.read_url(application_url(id))
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
# Turn a query string of the form "foo=bar&hello=sir" to {foo: "bar", hello: sir"}
|
72
|
+
def self.query_to_options(query)
|
73
|
+
options = {}
|
74
|
+
if query
|
75
|
+
query.split("&").each do |t|
|
76
|
+
key, value = t.split("=")
|
77
|
+
options[key.to_sym] = value
|
78
|
+
end
|
79
|
+
end
|
80
|
+
options
|
13
81
|
end
|
14
82
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
83
|
+
# Turn an options hash of the form {foo: "bar", hello: "sir"} into a query
|
84
|
+
# string of the form "foo=bar&hello=sir"
|
85
|
+
def self.options_to_query(options)
|
86
|
+
if options.empty?
|
87
|
+
nil
|
88
|
+
else
|
89
|
+
options.sort{|a,b| a.first.to_s <=> b.first.to_s}.map{|k,v| "#{k}=#{v}"}.join("&")
|
90
|
+
end
|
19
91
|
end
|
20
92
|
end
|
21
93
|
end
|
data/lib/atdis/model.rb
CHANGED
@@ -7,46 +7,18 @@ module ATDIS
|
|
7
7
|
|
8
8
|
included do
|
9
9
|
class_attribute :attribute_types
|
10
|
-
class_attribute :field_mappings
|
11
10
|
end
|
12
11
|
|
13
12
|
module ClassMethods
|
14
|
-
# of the form {:
|
15
|
-
def casting_attributes(p)
|
16
|
-
define_attribute_methods(p.keys.map{|k| k.to_s})
|
17
|
-
self.attribute_types = p
|
18
|
-
end
|
19
|
-
|
13
|
+
# of the form {section: Fixnum, address: String}
|
20
14
|
def set_field_mappings(p)
|
21
|
-
|
22
|
-
#
|
23
|
-
self.
|
24
|
-
casting_attributes(b)
|
25
|
-
end
|
26
|
-
|
27
|
-
private
|
28
|
-
|
29
|
-
def leaf_array?(v)
|
30
|
-
if !v.kind_of?(Array)
|
31
|
-
return false
|
32
|
-
end
|
33
|
-
v.all?{|a| !a.kind_of?(Array)}
|
34
|
-
end
|
35
|
-
|
36
|
-
def translate_field_mappings(p)
|
37
|
-
f = ActiveSupport::OrderedHash.new
|
38
|
-
ca = ActiveSupport::OrderedHash.new
|
15
|
+
define_attribute_methods(p.keys.map{|k| k.to_s})
|
16
|
+
# Convert all values to arrays. Doing this for the sake of tidier notation
|
17
|
+
self.attribute_types = {}
|
39
18
|
p.each do |k,v|
|
40
|
-
|
41
|
-
|
42
|
-
ca[v.first] = v[1..-1]
|
43
|
-
else
|
44
|
-
f2, ca2 = translate_field_mappings(v)
|
45
|
-
f[k] = f2
|
46
|
-
ca = ca.merge(ca2)
|
47
|
-
end
|
19
|
+
v = [v] unless v.kind_of?(Array)
|
20
|
+
self.attribute_types[k] = v
|
48
21
|
end
|
49
|
-
[f, ca]
|
50
22
|
end
|
51
23
|
end
|
52
24
|
end
|
@@ -74,105 +46,89 @@ module ATDIS
|
|
74
46
|
# Stores any part of the json that could not be interpreted. Usually
|
75
47
|
# signals an error if it isn't empty.
|
76
48
|
attr_accessor :json_left_overs, :json_load_error
|
77
|
-
|
78
|
-
validate :json_left_overs_is_empty
|
49
|
+
attr_accessor :url
|
79
50
|
|
80
|
-
|
81
|
-
|
82
|
-
end
|
51
|
+
validate :json_loaded_correctly!
|
52
|
+
validate :json_left_overs_is_empty
|
83
53
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
54
|
+
# Partition the data into used and unused by returning [used, unused]
|
55
|
+
def self.partition_by_used(data)
|
56
|
+
used, unused = {}, {}
|
57
|
+
if data.respond_to?(:each)
|
58
|
+
data.each do |key, value|
|
59
|
+
if attribute_keys.include?(key)
|
60
|
+
used[key] = value
|
61
|
+
else
|
62
|
+
unused[key] = value
|
93
63
|
end
|
94
64
|
end
|
65
|
+
else
|
66
|
+
unused = data
|
95
67
|
end
|
96
|
-
|
68
|
+
[used, unused]
|
97
69
|
end
|
98
70
|
|
99
|
-
def self.
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
elsif v.kind_of?(Hash) && data.has_key?(k)
|
104
|
-
r = map_field(key, data[k], mappings[k])
|
105
|
-
if r
|
106
|
-
return r
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
110
|
-
nil
|
71
|
+
def self.read_url(url)
|
72
|
+
r = read_json(RestClient.get(url.to_s).to_str)
|
73
|
+
r.url = url.to_s
|
74
|
+
r
|
111
75
|
end
|
112
76
|
|
113
|
-
def self.
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
else
|
122
|
-
json_left_overs[key] = data[key]
|
123
|
-
end
|
77
|
+
def self.read_json(text)
|
78
|
+
begin
|
79
|
+
data = MultiJson.load(text, symbolize_keys: true)
|
80
|
+
interpret(data)
|
81
|
+
rescue MultiJson::LoadError => e
|
82
|
+
a = interpret({response: []})
|
83
|
+
a.json_load_error = e.to_s
|
84
|
+
a
|
124
85
|
end
|
125
|
-
json_left_overs
|
126
86
|
end
|
127
87
|
|
128
|
-
def self.
|
129
|
-
|
130
|
-
|
131
|
-
if v.kind_of?(Hash)
|
132
|
-
result += attribute_names_from_mappings(v)
|
133
|
-
else
|
134
|
-
result << v
|
135
|
-
end
|
136
|
-
end
|
137
|
-
result
|
88
|
+
def self.interpret(*params)
|
89
|
+
used, unused = partition_by_used(*params)
|
90
|
+
new(used.merge(json_left_overs: unused))
|
138
91
|
end
|
139
92
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
attribute_names_from_mappings(mappings).each do |attribute|
|
144
|
-
values[attribute] = map_field(attribute, data, mappings)
|
93
|
+
def json_loaded_correctly!
|
94
|
+
if json_load_error
|
95
|
+
errors.add(:json, ErrorMessage["Invalid JSON: #{json_load_error}", nil])
|
145
96
|
end
|
146
|
-
values
|
147
97
|
end
|
148
98
|
|
149
|
-
def
|
99
|
+
def json_errors_local
|
150
100
|
r = []
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
r << [
|
101
|
+
# First show special json error
|
102
|
+
if !errors[:json].empty?
|
103
|
+
r << [nil, errors[:json]]
|
104
|
+
end
|
105
|
+
errors.keys.each do |attribute|
|
106
|
+
# The :json attribute is special
|
107
|
+
if attribute != :json
|
108
|
+
e = errors[attribute]
|
109
|
+
r << [{attribute => attributes_before_type_cast[attribute.to_s]}, e.map{|m| ErrorMessage["#{attribute} #{m}", m.spec_section]}] unless e.empty?
|
160
110
|
end
|
161
111
|
end
|
162
112
|
r
|
163
113
|
end
|
164
114
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
115
|
+
def json_errors_in_children
|
116
|
+
r = []
|
117
|
+
attributes.each do |attribute_as_string, value|
|
118
|
+
attribute = attribute_as_string.to_sym
|
119
|
+
e = errors[attribute]
|
120
|
+
if value.respond_to?(:json_errors)
|
121
|
+
r += value.json_errors.map{|a, b| [{attribute => a}, b]}
|
122
|
+
elsif value.kind_of?(Array)
|
123
|
+
f = value.find{|v| v.respond_to?(:json_errors) && !v.json_errors.empty?}
|
124
|
+
r += f.json_errors.map{|a, b| [{attribute => [a]}, b]} if f
|
173
125
|
end
|
174
126
|
end
|
175
|
-
|
127
|
+
r
|
128
|
+
end
|
129
|
+
|
130
|
+
def json_errors
|
131
|
+
json_errors_local + json_errors_in_children
|
176
132
|
end
|
177
133
|
|
178
134
|
# Have we tried to use this attribute?
|
@@ -180,14 +136,6 @@ module ATDIS
|
|
180
136
|
!attributes_before_type_cast[a].nil?
|
181
137
|
end
|
182
138
|
|
183
|
-
def level_used_locally?(level)
|
184
|
-
self.class.level_attribute_names(level).any?{|a| used_attribute?(a)}
|
185
|
-
end
|
186
|
-
|
187
|
-
def level_used?(level)
|
188
|
-
level_used_locally?(level) || level_used_in_children?(level)
|
189
|
-
end
|
190
|
-
|
191
139
|
def json_left_overs_is_empty
|
192
140
|
if json_left_overs && !json_left_overs.empty?
|
193
141
|
# We have extra parameters that shouldn't be there
|
@@ -202,24 +150,22 @@ module ATDIS
|
|
202
150
|
end if params
|
203
151
|
end
|
204
152
|
|
153
|
+
def self.attribute_keys
|
154
|
+
attribute_types.keys
|
155
|
+
end
|
156
|
+
|
205
157
|
# Does what the equivalent on Activerecord does
|
206
158
|
def self.attribute_names
|
207
159
|
attribute_types.keys.map{|k| k.to_s}
|
208
160
|
end
|
209
161
|
|
210
|
-
def self.
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
def self.cast(value, type, options = {})
|
215
|
-
if options[:none_is_nil] && value == "none"
|
216
|
-
nil
|
217
|
-
# If it's already the correct type then we don't need to do anything
|
218
|
-
elsif value.kind_of?(type)
|
162
|
+
def self.cast(value, type)
|
163
|
+
# If it's already the correct type (or nil) then we don't need to do anything
|
164
|
+
if value.nil? || value.kind_of?(type)
|
219
165
|
value
|
220
166
|
# Special handling for arrays. When we typecast arrays we actually typecast each member of the array
|
221
167
|
elsif value.kind_of?(Array)
|
222
|
-
value.map {|v| cast(v, type
|
168
|
+
value.map {|v| cast(v, type)}
|
223
169
|
elsif type == DateTime
|
224
170
|
cast_datetime(value)
|
225
171
|
elsif type == URI
|
@@ -250,7 +196,7 @@ module ATDIS
|
|
250
196
|
|
251
197
|
def attribute=(attr, value)
|
252
198
|
@attributes_before_type_cast[attr] = value
|
253
|
-
@attributes[attr] = Model.cast(value, attribute_types[attr.to_sym][0]
|
199
|
+
@attributes[attr] = Model.cast(value, attribute_types[attr.to_sym][0])
|
254
200
|
end
|
255
201
|
|
256
202
|
def self.cast_datetime(value)
|
@@ -287,7 +233,7 @@ module ATDIS
|
|
287
233
|
RGeo::GeoJSON.decode(hash_symbols_to_string(value))
|
288
234
|
end
|
289
235
|
|
290
|
-
# Converts {:
|
236
|
+
# Converts {foo: {bar: "yes"}} to {"foo" => {"bar" => "yes"}}
|
291
237
|
def self.hash_symbols_to_string(hash)
|
292
238
|
if hash.respond_to?(:each_pair)
|
293
239
|
result = {}
|