atdis 0.2 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -1
  3. data/.travis.yml +0 -1
  4. data/Gemfile +1 -1
  5. data/README.md +25 -2
  6. data/Rakefile +1 -1
  7. data/docs/ATDIS-1.0.2 Application Tracking Data Interchange Specification (v1.0.2).doc +0 -0
  8. data/docs/ATDIS-1.0.2 Application Tracking Data Interchange Specification (v1.0.2).pdf +0 -0
  9. data/lib/atdis.rb +2 -7
  10. data/lib/atdis/feed.rb +80 -8
  11. data/lib/atdis/model.rb +74 -128
  12. data/lib/atdis/models/address.rb +17 -0
  13. data/lib/atdis/models/application.rb +31 -0
  14. data/lib/atdis/models/authority.rb +19 -0
  15. data/lib/atdis/models/document.rb +16 -0
  16. data/lib/atdis/models/event.rb +16 -0
  17. data/lib/atdis/models/info.rb +81 -0
  18. data/lib/atdis/models/land_title_ref.rb +26 -0
  19. data/lib/atdis/models/location.rb +23 -0
  20. data/lib/atdis/models/page.rb +56 -0
  21. data/lib/atdis/models/pagination.rb +68 -0
  22. data/lib/atdis/models/person.rb +14 -0
  23. data/lib/atdis/models/reference.rb +13 -0
  24. data/lib/atdis/models/response.rb +16 -0
  25. data/lib/atdis/models/torrens_title.rb +24 -0
  26. data/lib/atdis/validators.rb +26 -10
  27. data/lib/atdis/version.rb +1 -1
  28. data/spec/atdis/feed_spec.rb +78 -22
  29. data/spec/atdis/model_spec.rb +80 -131
  30. data/spec/atdis/models/address_spec.rb +22 -0
  31. data/spec/atdis/models/application_spec.rb +246 -0
  32. data/spec/atdis/models/authority_spec.rb +34 -0
  33. data/spec/atdis/models/document_spec.rb +19 -0
  34. data/spec/atdis/models/event_spec.rb +29 -0
  35. data/spec/atdis/models/info_spec.rb +303 -0
  36. data/spec/atdis/models/land_title_ref_spec.rb +39 -0
  37. data/spec/atdis/models/location_spec.rb +95 -0
  38. data/spec/atdis/models/page_spec.rb +296 -0
  39. data/spec/atdis/models/pagination_spec.rb +153 -0
  40. data/spec/atdis/models/person_spec.rb +19 -0
  41. data/spec/atdis/models/reference_spec.rb +55 -0
  42. data/spec/atdis/models/response_spec.rb +5 -0
  43. data/spec/atdis/models/torrens_title_spec.rb +52 -0
  44. data/spec/atdis/separated_url_spec.rb +4 -4
  45. metadata +141 -135
  46. data/docs/ATDIS-1.0.7 Application Tracking Data Interchange Specification (v1.0).doc +0 -0
  47. data/docs/ATDIS-1.0.7 Application Tracking Data Interchange Specification (v1.0).pdf +0 -0
  48. data/lib/atdis/application.rb +0 -78
  49. data/lib/atdis/document.rb +0 -14
  50. data/lib/atdis/event.rb +0 -17
  51. data/lib/atdis/location.rb +0 -21
  52. data/lib/atdis/page.rb +0 -130
  53. data/lib/atdis/person.rb +0 -12
  54. data/spec/atdis/application_spec.rb +0 -539
  55. data/spec/atdis/document_spec.rb +0 -19
  56. data/spec/atdis/event_spec.rb +0 -29
  57. data/spec/atdis/location_spec.rb +0 -148
  58. data/spec/atdis/page_spec.rb +0 -492
  59. 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.8.7-p370
1
+ ruby-2.0.0-p353
data/.travis.yml CHANGED
@@ -1,6 +1,5 @@
1
1
  language: ruby
2
2
  rvm:
3
- - "1.8.7"
4
3
  - "1.9.3"
5
4
  - "2.0.0"
6
5
  # uncomment this line if your project needs to run something other than `rake`:
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', :require => false
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 **highly alpha** software that probably doesn't yet do what it says on the tin. It is very much a work in progress.
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
- TODO: Write usage instructions here
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
@@ -5,4 +5,4 @@ require 'rspec/core/rake_task'
5
5
  RSpec::Core::RakeTask.new('spec')
6
6
 
7
7
  # If you want to make this the default task
8
- task :default => :spec
8
+ task default: :spec
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 is the concatenation of the protocol and web address as defined in section 4.2 of specification
9
- # For example if the base_url is "http://www.council.nsw.gov.au" then the url for listing all the
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.kind_of?(URI) ? base_url : URI.parse(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
- def applications(page = 1)
16
- url = base_url + "atdis/1.0/applications.json"
17
- url += "?page=#{page}" if page > 1
18
- Page.read_url(url)
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 {:section=>[String, {:none_is_nil=>true}], :address=>[String]}
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
- a, b = translate_field_mappings(p)
22
- # field_mappings is of the form {:pagination=>{:previous=>:previous_page_no, :pages=>:total_no_pages}}
23
- self.field_mappings = a
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
- if leaf_array?(v)
41
- f[k] = v[0]
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
- def self.level_attribute_names(level)
81
- attribute_types.find_all{|k,v| (v[1] || {})[:level] == level }.map{|k,v| k.to_s}
82
- end
51
+ validate :json_loaded_correctly!
52
+ validate :json_left_overs_is_empty
83
53
 
84
- def json_attribute(a, new_value, mappings = field_mappings)
85
- mappings.each do |attribute, v|
86
- if v == a
87
- return {attribute => new_value}
88
- end
89
- if v.kind_of?(Hash)
90
- r = json_attribute(a, new_value, v)
91
- if r
92
- return {attribute => r}
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
- nil
68
+ [used, unused]
97
69
  end
98
70
 
99
- def self.map_field(key, data, mappings)
100
- mappings.each do |k, v|
101
- if v == key
102
- return data[k]
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.unused_data(data, mappings = field_mappings)
114
- json_left_overs = {}
115
- data.each_key do |key|
116
- if mappings[key]
117
- if mappings[key].kind_of?(Hash)
118
- l2 = unused_data(data[key], mappings[key])
119
- json_left_overs[key] = l2 unless l2.empty?
120
- end
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.attribute_names_from_mappings(mappings)
129
- result = []
130
- mappings.each do |k, v|
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
- # Map json structure to our values
141
- def self.map_fields(data, mappings = field_mappings)
142
- values = {}
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 json_errors
99
+ def json_errors_local
150
100
  r = []
151
- errors.messages.each do |attribute, e|
152
- value = attributes[attribute.to_s]
153
- if (value.respond_to?(:valid?) && !value.valid?)
154
- r += value.json_errors.map{|a, b| [json_attribute(attribute, a), b]}
155
- elsif (value && !value.respond_to?(:valid?) && value.respond_to?(:all?) && !value.all?{|v| v.valid?})
156
- f = value.find{|v| !v.valid?}
157
- r += f.json_errors.map{|a, b| [json_attribute(attribute, a), b]}
158
- else
159
- r << [json_attribute(attribute, attributes_before_type_cast[attribute.to_s]), e]
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
- # TODO This is doing a similar stepping down into the children that json_errors is doing. Would be nice
166
- # to extract the commond code to make this less horrible and arbitrary
167
- def level_used_in_children?(level)
168
- attributes.each_value do |a|
169
- if a.respond_to?(:level_used?) && a.level_used?(level)
170
- return true
171
- elsif a.kind_of?(Array) && a.any?{|b| b.level_used?(level)}
172
- return true
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
- false
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.interpret(*params)
211
- new(map_fields(*params).merge(:json_left_overs => unused_data(*params)))
212
- end
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, options)}
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], attribute_types[attr.to_sym][1] || {})
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 {:foo => {:bar => "yes"}} to {"foo" => {"bar" => "yes"}}
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 = {}