Service time zones in Ruby and Postgis

In one of the projects in which I participated, arose the problem of determining the time zone for the current geolocation of the user. On the backend came a record generated by the user through the smartphone. The time has come not in UTC, but the provided coordinates.
Of course, there are services (like The Google Time Zone), but they all paid or are severely limited in functionality. So I decided to write my own.

The service should be as simple as possible. We need to make only one request of the form
the
http://host/timezone/name?lat=55.2341&lng=43.23352

Where lat is the latitude, lng — longitude.

Nastraivaet database

As the database will use PostgreSQL. We will also need extension Postgis custom-tailored to work with geographic objects.

We assume that PostgreSQL is already installed. If not, the Internet has a lot of guides and tutorials on how to do it. The process of installing Postgis also should not cause difficulties on the official website has detailed instructions for most popular operating systems.

After installing everything, create a new database, which we will use to determine the time zone. Handling example, I write "tz_service":
the
CREATE DATABASE tz_service

Enable Postgis in your database:
the
CREATE EXTENSION postgis;
CREATE EXTENSION postgis_topology;
CREATE EXTENSION fuzzystrmatch;
CREATE EXTENSION postgis_tiger_geocoder;

Now we need a Shapefile of all time zones with the efele.net. Download tz_world.zip. The archive is a file tz_world.shp. Shape files contain vector representation of geographic data. But we need to convert it to an SQL dump and roll it to our base "tz_service":
the
$ /usr/lib/postgresql/9.1/bin/shp2pgsql-D tz_world.shp > dump.sql
$ psql -d tz_service -f dump.sql

Ready! I approve the request:
the
SELECT tzid FROM tz_world WHERE ST_Contains(the_geom, ST_MakePoint(-122.420706, 37.776685));

You should get something like this:
the
tzid 
---------------------
America/Los_Angeles
(1 ROW)

Write sevris on Ruby

As a skeleton service will use the framework Grape. It is great to quickly write REST-like server applications.
First, create a Gemfile and write there the gems we need:
the
source "https://rubygems.org"

gem 'rake'
gem 'activerecord'
gem 'pg'
gem 'grape'

group :development, :test do
gem 'shotgun'

gem 'byebug'
gem 'pry'
gem 'pry-byebug'

gem 'rspec'
end

What is the group development and test is necessary only for development and in production mode will not be used. But we need to develop not so much:
— shotgun that would not restart every time the server after the next code change
— buebug and pry for debugging
— rspec for tests

Install all gems with dependencies:
the
$ bundle install 

The project tree should look like this:

image

Let's go in order. Let's start with the configs.

In config/database.yml will contain information for communication with the database:
the
development &config
adapter: postgresql
host: localhost
username: user
password: password
database: tz_service
encoding: utf8

test:
<<: *config

poduction:
<<: *config

Next put the configuration class database config/configuration.rb for parsing yaml file:
the
class Configuration

DB_CONFIG = YAML.load_file(File.expand_path('../database.yml', __FILE__))[ENV['NOT']]

class << self
def adapter
DB_CONFIG['adapter']
end

def host
DB_CONFIG['host']
end

def username
DB_CONFIG['username']
end

def password
DB_CONFIG['password']
end

def database
DB_CONFIG['database']
end

def encoding
DB_CONFIG['encoding']
end
end
end

In the app/environment.rb will contain environment settings:
the
require 'bundler'
Bundler.require(:default)

$: << File.expand_path('../', __FILE__)
$: << File.expand_path('../../', __FILE__)
$: << File.expand_path('../../config', __FILE__)
$: << File.expand_path('../services', __FILE__)

ENV['NOT'] ||= 'development'

require 'grape'
require 'json'
require 'pry'
require 'active_record'

require 'timezone_name_service'
require 'configuration'

require 'time_zone_api'

In app/application.rb set configure activerecord to connect to the database:
the
ActiveRecord::Base.establish_connection(
adapter Configuration.adapter,
host: Configuration.host
database: Configuration.database
username: Configuration.username
password: Configuration.password
encoding: Configuration.encoding
)

The basis for the service ready, it is only necessary to write one class of the service, which will answer our request and all. All? No! First you need to write tests. Do not forget about TDD.

Sosedin spec/spec_helper.rb and his moods a bit:
the
ENV['NOT'] ||= 'test'

require_relative '../app/environment'

require 'rack/test'

RSpec.configure do |config|
config.treat_symbols_as_metadata_keys_with_true_values = true
config.run_all_when_everything_filtered = true
config.filter_run :focus

config.order = 'random'
config.include Rack::Test::Methods

def app
TimeZoneAPI
end

end

In the tests we shall describe the behaviour of the service. But we expect only two things:
1. An adequate response with the appropriate parameters in the query
2. Error when no parameter
Describe it:
the
describe 'API' do

let(:params) {
{
lat: 55.7914056,
lng: 49.1120427
}
} #send the parameters in the query

let(:error) {
{ error: 'lat is missing, lng is missing' }
} #the expected answer error parsing parameters

let(:name_response) {
{ timezone: 'Europe/Moscow' }
} #the expected response on successful request

#Home
it 'should greet us' do
get '/'

expect(last_response).to be_ok
expect(last_response.body).to eq(%Q{"Welcome to Time Zone API Service"})
end

#Description of the process of getting the name of the time zone
describe 'Timezone name' do
subject {
last_response
}

#Description of various situations in contexts
context 'with wrong params' do
before do
get '/timezone/name'
end

its(:status) {should eq 400}
its(:body) {should eq "error".to_json}
end

context 'with right params' do
before do
get '/timezone/name', params
end

its(:status) {should eq 200}
its(:body) {should eq name_response.to_json}
end

end

end

By running the command:
the
$ bundle exec rspec

No test will pass. Another would be =) Have to zaselenie tests.
We need to address in database with custom query. We will do this through the class app/services/time_zone_service.rb:
the
class TimezoneNameService

def initialize(lat, lng)
@lat = lat
@lng = lng
end

def name
#"Non-standard" request. To screen coordinates does not make sense, because validation will occur when parsing
sql = "SELECT tzid FROM tz_world WHERE ST_Contains(geom, ST_MakePoint(#{ActiveRecord::Base.sanitize(@lang)}, #{ActiveRecord::Base.sanitize(@lat)}));"
name_hash = ActiveRecord::Base.connection.execute(sql).first

name_hash['tzid'] rescue nil
end
end

And finally, the main class of the app/time_zone_api.rb:
the
class TimeZoneAPI < Grape::API

format :json
default_format :json
default_error_formatter :txt

desc 'Start page'
get '/' do
'Welcome to Time Zone API Service'
end

namespace 'timezone' do

desc 'Time zone name by coordinates'
params do
requires :lat, type: Float, desc: 'Latitude'
requires :lng, type: Float, desc: 'Longitude'
end #Validation settings
get '/name' do
name = TimezoneNameService.new(params[:lat], params[:lng]).name

{ timezone-name }
end

end

end

That's all! The service is ready. To test it "live" can be running Grape-app:
the
$ bundle exec rackup

related Links
Framework Grape
Postgis
Code project at Github
Article based on information from habrahabr.ru

Комментарии

Популярные сообщения из этого блога

Integration of PostgreSQL with MS SQL Server for those who want faster and deeper

Custom database queries in MODx Revolution

Parse URL in Zend Framework 2