atdis 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-1.8.7-p370
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - "1.8.7"
4
+ - "1.9.3"
5
+ - "2.0.0"
6
+ # uncomment this line if your project needs to run something other than `rake`:
7
+ # script: bundle exec rspec spec
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
@@ -0,0 +1,7 @@
1
+ # More info at https://github.com/guard/guard#readme
2
+
3
+ guard 'rspec' do
4
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ end
7
+
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
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new('spec')
6
+
7
+ # If you want to make this the default task
8
+ task :default => :spec
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
@@ -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
@@ -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
@@ -0,0 +1,10 @@
1
+ module ATDIS
2
+ class Person < Model
3
+ field_mappings :name => [:name, String],
4
+ :role => [:role, String],
5
+ :contact => [:contact, String]
6
+
7
+ # Mandatory parameters
8
+ validates :name, :role, :presence_before_type_cast => true
9
+ end
10
+ end
@@ -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