atdis 0.1

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.
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