atdis 0.2 → 0.3

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.
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 = {}