atdis 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +15 -0
- data/Guardfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +37 -0
- data/Rakefile +8 -0
- data/atdis.gemspec +28 -0
- data/docs/ATDIS-1.0.4 Application Tracking Data Interchange Specification.docx +0 -0
- data/docs/ATDIS-1.0.4 Application Tracking Data Interchange Specification.pdf +0 -0
- data/lib/atdis/application.rb +47 -0
- data/lib/atdis/document.rb +12 -0
- data/lib/atdis/event.rb +12 -0
- data/lib/atdis/feed.rb +21 -0
- data/lib/atdis/location.rb +18 -0
- data/lib/atdis/model.rb +190 -0
- data/lib/atdis/page.rb +114 -0
- data/lib/atdis/person.rb +10 -0
- data/lib/atdis/separated_url.rb +37 -0
- data/lib/atdis/validators.rb +51 -0
- data/lib/atdis/version.rb +3 -0
- data/lib/atdis.rb +12 -0
- data/spec/atdis/application_spec.rb +406 -0
- data/spec/atdis/document_spec.rb +15 -0
- data/spec/atdis/event_spec.rb +25 -0
- data/spec/atdis/feed_spec.rb +40 -0
- data/spec/atdis/location_spec.rb +97 -0
- data/spec/atdis/model_spec.rb +39 -0
- data/spec/atdis/page_spec.rb +390 -0
- data/spec/atdis/person_spec.rb +15 -0
- data/spec/atdis/separated_url_spec.rb +40 -0
- data/spec/spec_helper.rb +18 -0
- metadata +191 -0
data/.gitignore
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-1.8.7-p370
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
group :development do
|
4
|
+
gem "rspec"
|
5
|
+
gem 'guard'
|
6
|
+
gem 'guard-rspec'
|
7
|
+
gem 'growl'
|
8
|
+
gem 'rb-fsevent', '~> 0.9'
|
9
|
+
# Probably required on OS X. See https://github.com/guard/guard/wiki/Add-Readline-support-to-Ruby-on-Mac-OS-X
|
10
|
+
gem 'rb-readline'
|
11
|
+
gem 'coveralls', :require => false
|
12
|
+
end
|
13
|
+
|
14
|
+
# Specify your gem's dependencies in atdis.gemspec
|
15
|
+
gemspec
|
data/Guardfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 OpenAustralia Foundation Limited
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# Atdis
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.org/openaustralia/atdis.png?branch=master)](https://travis-ci.org/openaustralia/atdis) [![Coverage Status](https://coveralls.io/repos/openaustralia/atdis/badge.png?branch=master)](https://coveralls.io/r/openaustralia/atdis?branch=master) [![Code Climate](https://codeclimate.com/github/openaustralia/atdis.png)](https://codeclimate.com/github/openaustralia/atdis)
|
4
|
+
|
5
|
+
A ruby interface to the application tracking data interchange specification (ATDIS) API
|
6
|
+
|
7
|
+
We're developing this against version ATDIS 1.0.4.
|
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.
|
10
|
+
|
11
|
+
Source code is available on GitHub at https://github.com/openaustralia/atdis
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
gem 'atdis'
|
18
|
+
|
19
|
+
And then execute:
|
20
|
+
|
21
|
+
$ bundle
|
22
|
+
|
23
|
+
Or install it yourself as:
|
24
|
+
|
25
|
+
$ gem install atdis
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
TODO: Write usage instructions here
|
30
|
+
|
31
|
+
## Contributing
|
32
|
+
|
33
|
+
1. Fork it
|
34
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
35
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
36
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
37
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/atdis.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'atdis/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "atdis"
|
8
|
+
spec.version = Atdis::VERSION
|
9
|
+
spec.authors = ["Matthew Landauer"]
|
10
|
+
spec.email = ["matthew@openaustraliafoundation.org.au"]
|
11
|
+
spec.description = %q{A ruby interface to the application tracking data interchange specification (ATDIS) API}
|
12
|
+
spec.summary = spec.description
|
13
|
+
spec.homepage = "http://github.com/openaustralia/atdis"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
|
24
|
+
spec.add_dependency "multi_json", "~> 1.7"
|
25
|
+
spec.add_dependency "rest-client"
|
26
|
+
spec.add_dependency "rgeo-geojson"
|
27
|
+
spec.add_dependency "activemodel", "~> 3"
|
28
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'multi_json'
|
2
|
+
|
3
|
+
module ATDIS
|
4
|
+
class Application < Model
|
5
|
+
field_mappings :application => {
|
6
|
+
:info => {
|
7
|
+
:dat_id => [:dat_id, String],
|
8
|
+
:last_modified_date => [:last_modified_date, DateTime],
|
9
|
+
:description => [:description, String],
|
10
|
+
:authority => [:authority, String],
|
11
|
+
:lodgement_date => [:lodgement_date, DateTime],
|
12
|
+
:determination_date => [:determination_date, DateTime],
|
13
|
+
:status => [:status, String],
|
14
|
+
:notification_start_date => [:notification_start_date, DateTime],
|
15
|
+
:notification_end_date => [:notification_end_date, DateTime],
|
16
|
+
:officer => [:officer, String],
|
17
|
+
:estimated_cost => [:estimated_cost, String]
|
18
|
+
},
|
19
|
+
:reference => {
|
20
|
+
:more_info_url => [:more_info_url, URI],
|
21
|
+
:comments_url => [:comments_url, URI]
|
22
|
+
},
|
23
|
+
:location => [:location, Location],
|
24
|
+
:events => [:events, Event],
|
25
|
+
:documents => [:documents, Document],
|
26
|
+
:people => [:people, Person],
|
27
|
+
:extended => [:extended, Object]
|
28
|
+
}
|
29
|
+
|
30
|
+
# Mandatory parameters
|
31
|
+
validates :dat_id, :last_modified_date, :description, :authority, :lodgement_date, :determination_date, :status,
|
32
|
+
:more_info_url, :location, :presence_before_type_cast => true
|
33
|
+
|
34
|
+
# Other validations
|
35
|
+
validates :notification_start_date, :notification_end_date, :last_modified_date, :lodgement_date, :determination_date,
|
36
|
+
:date_time => true
|
37
|
+
validates :more_info_url, :http_url => true
|
38
|
+
validates :location, :valid => true
|
39
|
+
|
40
|
+
# TODO Validate associated like locations, events, documents, people
|
41
|
+
# TODO Do we need to do extra checking to ensure that events, documents and people are arrays?
|
42
|
+
# TODO Separate validation for L2 and L3 compliance?
|
43
|
+
# TODO Validate date orders. i.e. determination_date >= lodgement_date and notification_end_date >= notification_start_date
|
44
|
+
# TODO also last_modified_date >= lodgement_date and all the other dates. In other words we can't put a future date in. That
|
45
|
+
# doesn't make sense in this context. Also should check dates in things like Events (to see that they're not in the future)
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module ATDIS
|
2
|
+
class Document < Model
|
3
|
+
field_mappings :ref => [:ref, String],
|
4
|
+
:title => [:title, String],
|
5
|
+
:document_url => [:document_url, URI]
|
6
|
+
|
7
|
+
# Mandatory parameters
|
8
|
+
validates :ref, :title, :document_url, :presence_before_type_cast => true
|
9
|
+
# Other validations
|
10
|
+
validates :document_url, :http_url => true
|
11
|
+
end
|
12
|
+
end
|
data/lib/atdis/event.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
module ATDIS
|
2
|
+
class Event < Model
|
3
|
+
field_mappings :id => [:id, String],
|
4
|
+
:date => [:date, DateTime],
|
5
|
+
:description => [:description, String],
|
6
|
+
:event_type => [:event_type, String],
|
7
|
+
:status => [:status, String]
|
8
|
+
|
9
|
+
# Mandatory parameters
|
10
|
+
validates :id, :date, :description, :presence_before_type_cast => true
|
11
|
+
end
|
12
|
+
end
|
data/lib/atdis/feed.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require "rest-client"
|
2
|
+
|
3
|
+
module ATDIS
|
4
|
+
class Feed
|
5
|
+
attr_reader :base_url
|
6
|
+
|
7
|
+
# 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"
|
11
|
+
def initialize(base_url)
|
12
|
+
@base_url = base_url.kind_of?(URI) ? base_url : URI.parse(base_url)
|
13
|
+
end
|
14
|
+
|
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)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "rgeo/geo_json"
|
2
|
+
|
3
|
+
module ATDIS
|
4
|
+
class Location < Model
|
5
|
+
field_mappings :address => [:address, String],
|
6
|
+
:land_title_ref => {
|
7
|
+
:lot => [:lot, String],
|
8
|
+
:section => [:section, String],
|
9
|
+
:dpsp_id => [:dpsp_id, String]
|
10
|
+
},
|
11
|
+
:geometry => [:geometry, RGeo::GeoJSON]
|
12
|
+
|
13
|
+
# Mandatory parameters
|
14
|
+
validates :address, :lot, :section, :dpsp_id, :presence_before_type_cast => true
|
15
|
+
|
16
|
+
validates :geometry, :geo_json => true
|
17
|
+
end
|
18
|
+
end
|
data/lib/atdis/model.rb
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
module ATDIS
|
5
|
+
module TypeCastAttributes
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
class_attribute :attribute_types
|
10
|
+
class_attribute :valid_fields
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
def casting_attributes(p)
|
15
|
+
define_attribute_methods(p.keys.map{|k| k.to_s})
|
16
|
+
self.attribute_types = p
|
17
|
+
end
|
18
|
+
|
19
|
+
def field_mappings(p)
|
20
|
+
a, b = translate_field_mappings(p)
|
21
|
+
self.valid_fields = a
|
22
|
+
casting_attributes(b)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def translate_field_mappings(p)
|
28
|
+
f = {}
|
29
|
+
ca = {}
|
30
|
+
p.each do |k,v|
|
31
|
+
if v.kind_of?(Array)
|
32
|
+
f[k] = v[0]
|
33
|
+
ca[v.first] = v[1]
|
34
|
+
else
|
35
|
+
f2, ca2 = translate_field_mappings(v)
|
36
|
+
f[k] = f2
|
37
|
+
ca = ca.merge(ca2)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
[f, ca]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Model
|
46
|
+
include ActiveModel::Validations
|
47
|
+
include Validators
|
48
|
+
include ActiveModel::AttributeMethods
|
49
|
+
include TypeCastAttributes
|
50
|
+
attribute_method_suffix '_before_type_cast'
|
51
|
+
attribute_method_suffix '='
|
52
|
+
|
53
|
+
attr_reader :attributes
|
54
|
+
# Stores any part of the json that could not be interpreted. Usually
|
55
|
+
# signals an error if it isn't empty.
|
56
|
+
attr_accessor :json_left_overs
|
57
|
+
|
58
|
+
validate :json_left_overs_is_empty
|
59
|
+
|
60
|
+
def json_left_overs_is_empty
|
61
|
+
if json_left_overs && !json_left_overs.empty?
|
62
|
+
# We have extra parameters that shouldn't be there
|
63
|
+
errors.add(:json, "Unexpected parameters in json data: #{MultiJson.dump(json_left_overs)}")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def initialize(params={})
|
68
|
+
@attributes, @attributes_before_type_cast = {}, {}
|
69
|
+
params.each do |attr, value|
|
70
|
+
self.send("#{attr}=", value)
|
71
|
+
end if params
|
72
|
+
end
|
73
|
+
|
74
|
+
# Does what the equivalent on Activerecord does
|
75
|
+
def self.attribute_names
|
76
|
+
attribute_types.keys.map{|k| k.to_s}
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.interpret(*params)
|
80
|
+
new(map_fields(valid_fields, *params))
|
81
|
+
end
|
82
|
+
|
83
|
+
# Map json structure to our values
|
84
|
+
def self.map_fields(valid_fields, data)
|
85
|
+
values = {:json_left_overs => {}}
|
86
|
+
data.each_key do |key|
|
87
|
+
if valid_fields[key]
|
88
|
+
if valid_fields[key].kind_of?(Hash)
|
89
|
+
v2 = map_fields(valid_fields[key], data[key])
|
90
|
+
l2 = v2.delete(:json_left_overs)
|
91
|
+
values = values.merge(v2)
|
92
|
+
values[:json_left_overs][key] = l2 unless l2.empty?
|
93
|
+
else
|
94
|
+
values[valid_fields[key]] = data[key]
|
95
|
+
end
|
96
|
+
else
|
97
|
+
values[:json_left_overs][key] = data[key]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
values
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.cast(value, type)
|
104
|
+
# If it's already the correct type then we don't need to do anything
|
105
|
+
if value.kind_of?(type)
|
106
|
+
value
|
107
|
+
# Special handling for arrays. When we typecast arrays we actually typecast each member of the array
|
108
|
+
elsif value.kind_of?(Array)
|
109
|
+
value.map {|v| cast(v, type)}
|
110
|
+
elsif type == DateTime
|
111
|
+
cast_datetime(value)
|
112
|
+
elsif type == URI
|
113
|
+
cast_uri(value)
|
114
|
+
elsif type == String
|
115
|
+
cast_string(value)
|
116
|
+
elsif type == Fixnum
|
117
|
+
cast_fixnum(value)
|
118
|
+
elsif type == RGeo::GeoJSON
|
119
|
+
cast_geojson(value)
|
120
|
+
# Otherwise try to use Type.interpret to do the typecasting
|
121
|
+
elsif type.respond_to?(:interpret)
|
122
|
+
type.interpret(value) if value
|
123
|
+
else
|
124
|
+
raise
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def attribute(attr)
|
131
|
+
@attributes[attr]
|
132
|
+
end
|
133
|
+
|
134
|
+
def attribute_before_type_cast(attr)
|
135
|
+
@attributes_before_type_cast[attr]
|
136
|
+
end
|
137
|
+
|
138
|
+
def attribute=(attr, value)
|
139
|
+
@attributes_before_type_cast[attr] = value
|
140
|
+
@attributes[attr] = Model.cast(value, attribute_types[attr.to_sym])
|
141
|
+
end
|
142
|
+
|
143
|
+
def self.cast_datetime(value)
|
144
|
+
# This would be much easier if we knew we only had to support Ruby 1.9 or greater because it has
|
145
|
+
# an implementation built in. Because for the time being we need to support Ruby 1.8 as well
|
146
|
+
# we'll build an implementation of parsing by hand. Ugh.
|
147
|
+
# Referencing http://www.w3.org/TR/NOTE-datetime
|
148
|
+
# In section 4.3.1 of ATDIS 1.0.4 it shows two variants of iso 8601, either the full date
|
149
|
+
# or the full date with hours, seconds, minutes and timezone. We'll assume that these
|
150
|
+
# are the two variants that are allowed.
|
151
|
+
if value.respond_to?(:match) && value.match(/^\d\d\d\d-\d\d-\d\d(T\d\d:\d\d:\d\d(Z|(\+|-)\d\d:\d\d))?$/)
|
152
|
+
DateTime.parse(value)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def self.cast_uri(value)
|
157
|
+
begin
|
158
|
+
URI.parse(value)
|
159
|
+
rescue URI::InvalidURIError
|
160
|
+
nil
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def self.cast_string(value)
|
165
|
+
value.to_s
|
166
|
+
end
|
167
|
+
|
168
|
+
# This casting allows nil values
|
169
|
+
def self.cast_fixnum(value)
|
170
|
+
value.to_i if value
|
171
|
+
end
|
172
|
+
|
173
|
+
def self.cast_geojson(value)
|
174
|
+
RGeo::GeoJSON.decode(hash_symbols_to_string(value))
|
175
|
+
end
|
176
|
+
|
177
|
+
# Converts {:foo => {:bar => "yes"}} to {"foo" => {"bar" => "yes"}}
|
178
|
+
def self.hash_symbols_to_string(hash)
|
179
|
+
if hash.respond_to?(:each_pair)
|
180
|
+
result = {}
|
181
|
+
hash.each_pair do |key, value|
|
182
|
+
result[key.to_s] = hash_symbols_to_string(value)
|
183
|
+
end
|
184
|
+
result
|
185
|
+
else
|
186
|
+
hash
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
data/lib/atdis/page.rb
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
module ATDIS
|
2
|
+
class Page < Model
|
3
|
+
attr_accessor :url
|
4
|
+
|
5
|
+
field_mappings :response => [:results, Application],
|
6
|
+
:count => [:count, Fixnum],
|
7
|
+
:pagination => {
|
8
|
+
:previous => [:previous_page_no, Fixnum],
|
9
|
+
:next => [:next_page_no, Fixnum],
|
10
|
+
:current => [:current_page_no, Fixnum],
|
11
|
+
:per_page => [:no_results_per_page, Fixnum],
|
12
|
+
:count => [:total_no_results, Fixnum],
|
13
|
+
:pages => [:total_no_pages, Fixnum]
|
14
|
+
}
|
15
|
+
|
16
|
+
# Mandatory parameters
|
17
|
+
validates :results, :presence_before_type_cast => true
|
18
|
+
validates :results, :valid => true
|
19
|
+
validate :count_is_consistent, :all_pagination_is_present, :previous_page_no_is_consistent, :next_page_no_is_consistent
|
20
|
+
validate :current_page_no_is_consistent, :total_no_results_is_consistent
|
21
|
+
|
22
|
+
# If some of the pagination fields are present all of the required ones should be present
|
23
|
+
def all_pagination_is_present
|
24
|
+
if count || previous_page_no || next_page_no || current_page_no || no_results_per_page ||
|
25
|
+
total_no_results || total_no_pages
|
26
|
+
errors.add(:count, "should be present if pagination is being used") if count.nil?
|
27
|
+
errors.add(:current_page_no, "should be present if pagination is being used") if current_page_no.nil?
|
28
|
+
errors.add(:no_results_per_page, "should be present if pagination is being used") if no_results_per_page.nil?
|
29
|
+
errors.add(:total_no_results, "should be present if pagination is being used") if total_no_results.nil?
|
30
|
+
errors.add(:total_no_pages, "should be present if pagination is being used") if total_no_pages.nil?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def count_is_consistent
|
35
|
+
if count
|
36
|
+
errors.add(:count, "is not the same as the number of applications returned") if count != results.count
|
37
|
+
errors.add(:count, "should not be larger than the number of results per page") if count > no_results_per_page
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def previous_page_no_is_consistent
|
42
|
+
if current_page_no
|
43
|
+
if previous_page_no
|
44
|
+
if previous_page_no != current_page_no - 1
|
45
|
+
errors.add(:previous_page_no, "should be one less than current page number or null if first page")
|
46
|
+
end
|
47
|
+
if current_page_no == 1
|
48
|
+
errors.add(:previous_page_no, "should be null if on the first page")
|
49
|
+
end
|
50
|
+
else
|
51
|
+
if current_page_no > 1
|
52
|
+
errors.add(:previous_page_no, "can't be null if not on the first page")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def next_page_no_is_consistent
|
59
|
+
if next_page_no && next_page_no != current_page_no + 1
|
60
|
+
errors.add(:next_page_no, "should be one greater than current page number or null if last page")
|
61
|
+
end
|
62
|
+
if next_page_no.nil? && current_page_no != total_no_pages
|
63
|
+
errors.add(:next_page_no, "can't be null if not on the last page")
|
64
|
+
end
|
65
|
+
if next_page_no && current_page_no == total_no_pages
|
66
|
+
errors.add(:next_page_no, "should be null if on the last page")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def current_page_no_is_consistent
|
71
|
+
if current_page_no
|
72
|
+
errors.add(:current_page_no, "is larger than the number of pages") if current_page_no > total_no_pages
|
73
|
+
errors.add(:current_page_no, "can not be less than 1") if current_page_no < 1
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def total_no_results_is_consistent
|
78
|
+
if total_no_pages && total_no_results > total_no_pages * no_results_per_page
|
79
|
+
errors.add(:total_no_results, "is larger than can be retrieved through paging")
|
80
|
+
end
|
81
|
+
if total_no_pages && total_no_results <= (total_no_pages - 1) * no_results_per_page
|
82
|
+
errors.add(:total_no_results, "could fit into a smaller number of pages")
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.read_url(url)
|
87
|
+
r = read_json(RestClient.get(url.to_s).to_str)
|
88
|
+
r.url = url.to_s
|
89
|
+
r
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.read_json(text)
|
93
|
+
interpret(MultiJson.load(text, :symbolize_keys => true))
|
94
|
+
end
|
95
|
+
|
96
|
+
def previous_url
|
97
|
+
raise "Can't use previous_url when loaded with read_json" if url.nil?
|
98
|
+
ATDIS::SeparatedURL.merge(url, :page => previous_page_no) if previous_page_no
|
99
|
+
end
|
100
|
+
|
101
|
+
def next_url
|
102
|
+
raise "Can't use next_url when loaded with read_json" if url.nil?
|
103
|
+
ATDIS::SeparatedURL.merge(url, :page => next_page_no) if next_page_no
|
104
|
+
end
|
105
|
+
|
106
|
+
def previous
|
107
|
+
Page.read_url(previous_url) if previous_url
|
108
|
+
end
|
109
|
+
|
110
|
+
def next
|
111
|
+
Page.read_url(next_url) if next_url
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
data/lib/atdis/person.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
module ATDIS
|
2
|
+
class SeparatedURL
|
3
|
+
|
4
|
+
def self.merge(full_url, params)
|
5
|
+
url, url_params = split(full_url)
|
6
|
+
combine(url, url_params.merge(params))
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def self.combine(url, url_params)
|
12
|
+
# Doing this jiggery pokery to ensure the params are sorted alphabetically (even on Ruby 1.8)
|
13
|
+
query = url_params.map{|k,v| [k.to_s, v]}.sort.map{|k,v| "#{CGI.escape(k)}=#{CGI.escape(v.to_s)}"}.join("&")
|
14
|
+
if url_params.empty?
|
15
|
+
url
|
16
|
+
else
|
17
|
+
url + "?" + query
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.split(full_url)
|
22
|
+
uri = URI.parse(full_url)
|
23
|
+
if (uri.scheme == "http" && uri.port == 80) || (uri.scheme == "https" && uri.port == 443)
|
24
|
+
url = "#{uri.scheme}://#{uri.host}#{uri.path}"
|
25
|
+
else
|
26
|
+
url = "#{uri.scheme}://#{uri.host}:#{uri.port}#{uri.path}"
|
27
|
+
end
|
28
|
+
if uri.query
|
29
|
+
url_params = Hash[*CGI::parse(uri.query).map{|k,v| [k.to_sym,v.first]}.flatten]
|
30
|
+
else
|
31
|
+
url_params = {}
|
32
|
+
end
|
33
|
+
[url, url_params]
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
|
3
|
+
module ATDIS
|
4
|
+
module Validators
|
5
|
+
class GeoJsonValidator < ActiveModel::EachValidator
|
6
|
+
def validate_each(record, attribute, value)
|
7
|
+
raw_value = record.send("#{attribute}_before_type_cast")
|
8
|
+
if raw_value.present? && value.nil?
|
9
|
+
record.errors.add(attribute, "is not valid GeoJSON")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class DateTimeValidator < ActiveModel::EachValidator
|
15
|
+
def validate_each(record, attribute, value)
|
16
|
+
raw_value = record.send("#{attribute}_before_type_cast")
|
17
|
+
if raw_value.present? && !value.kind_of?(DateTime)
|
18
|
+
record.errors.add(attribute, "is not a valid date")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class HttpUrlValidator < ActiveModel::EachValidator
|
24
|
+
def validate_each(record, attribute, value)
|
25
|
+
raw_value = record.send("#{attribute}_before_type_cast")
|
26
|
+
if raw_value.present? && !value.kind_of?(URI::HTTP) && !value.kind_of?(URI::HTTPS)
|
27
|
+
record.errors.add(attribute, "is not a valid URL")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Take into account the value before type casting
|
33
|
+
class PresenceBeforeTypeCastValidator < ActiveModel::EachValidator
|
34
|
+
def validate_each(record, attribute, value)
|
35
|
+
raw_value = record.send("#{attribute}_before_type_cast")
|
36
|
+
unless raw_value.present?
|
37
|
+
record.errors.add(attribute, "can't be blank")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# This attribute itself needs to be valid
|
43
|
+
class ValidValidator < ActiveModel::EachValidator
|
44
|
+
def validate_each(record, attribute, value)
|
45
|
+
if (value.respond_to?(:valid?) && !value.valid?) || (value && !value.respond_to?(:valid?) && !value.all?{|v| v.valid?})
|
46
|
+
record.errors.add(attribute, "is not valid")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|