├── .circleci └── config.yml ├── .editorconfig ├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib ├── what3words.rb └── what3words │ ├── api.rb │ └── version.rb ├── sample └── sample.rb ├── spec ├── config.sample.yaml ├── lib │ └── what3words │ │ └── what3words_api_spec.rb └── spec_helper.rb └── what3words.gemspec /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | executors: 4 | ruby-executor: 5 | docker: 6 | - image: cimg/ruby:2.7.2 7 | 8 | jobs: 9 | ruby-test: 10 | executor: ruby-executor 11 | steps: 12 | - checkout 13 | - run: 14 | name: Install dependencies 15 | command: bundle install 16 | - run: 17 | name: Run tests 18 | command: bundle exec rspec 19 | 20 | workflows: 21 | test: 22 | jobs: 23 | - ruby-test 24 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | # Set default charset 13 | charset = utf-8 14 | 15 | # Indentation 16 | indent_style = space 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/ruby,macos 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=ruby,macos 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### macOS Patch ### 34 | # iCloud generated files 35 | *.icloud 36 | 37 | ### Ruby ### 38 | *.gem 39 | *.rbc 40 | /.config 41 | /coverage/ 42 | /InstalledFiles 43 | /pkg/ 44 | /spec/reports/ 45 | /spec/examples.txt 46 | /test/tmp/ 47 | /test/version_tmp/ 48 | /tmp/ 49 | 50 | # Used by dotenv library to load environment variables. 51 | # .env 52 | 53 | # Ignore Byebug command history file. 54 | .byebug_history 55 | 56 | ## Specific to RubyMotion: 57 | .dat* 58 | .repl_history 59 | build/ 60 | *.bridgesupport 61 | build-iPhoneOS/ 62 | build-iPhoneSimulator/ 63 | 64 | ## Specific to RubyMotion (use of CocoaPods): 65 | # 66 | # We recommend against adding the Pods directory to your .gitignore. However 67 | # you should judge for yourself, the pros and cons are mentioned at: 68 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 69 | # vendor/Pods/ 70 | 71 | ## Documentation cache and generated files: 72 | /.yardoc/ 73 | /_yardoc/ 74 | /doc/ 75 | /rdoc/ 76 | 77 | ## Environment normalization: 78 | /.bundle/ 79 | /vendor/bundle 80 | /lib/bundler/man/ 81 | 82 | # for a library or gem, you might want to ignore these files since the code is 83 | # intended to run in multiple environments; otherwise, check them in: 84 | # Gemfile.lock 85 | # .ruby-version 86 | # .ruby-gemset 87 | 88 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 89 | .rvmrc 90 | 91 | # Used by RuboCop. Remote config files pulled in from inherit_from directive. 92 | # .rubocop-https?--* 93 | 94 | # End of https://www.toptal.com/developers/gitignore/api/ruby,macos 95 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.4 4 | script: rake rubocop spec 5 | notifications: 6 | slack: 7 | secure: DvejcrOH6RdAdrGEPLUrnia9cES8iCHVlVSFAMwx2vaR/I3T9bfUiHnb3VHfiqx4cvDL+9wT3opG0sZkUCFJGqzA3E//VObKNXjwUFCUptFG68qgR7LZ3vDatWlUqU+32gWeYaHM3FpNP2E/IARjphaiF3jRUZbronqVzJZN2MbMsUYBeGq7v0AaLkgbxVgOWtBnMVcBPOnarEJqHyJlqQ2unYnIy+GNoOFtPMPNZobYTf0zxrycldYpP937yT4CTsY3I7RxuEtnY2sPYW+eFBESGLg6EfnjeOruROY+b9cSHIh1Qzr7Oup6a+oQ3+vCvLbYsGpEO04RTeTTtOGc7aRlUylmvuFPi7qQwOTZ+4KD5aIBh4p7bQiPfvAeqDU4pVJxPHHZirgjFpUpAvofj48SvDgwX9cFyAhAlDZ9qBOctmhphW9KNkcAW4MY421AdVAjFWwc5CU8G5oPdKcyWam3ZB3nHwKgZm6LLAcZgbG0rjZT+iPeQBvTKH3kPx7E1CkxZNkEralHJ3l+bDwY3GdzsgcMpT3bWNl3pLszFzgXljDScMU8my1+xjDQINEnI3TCJA4MhKkzYkRgfrEBkgPb2SiF7qnhbyMzSFbCU4jsM1MLQZolNG9EsjsxJLuL4YTC2wKOHajViUUeaCNIcNU5UXBQuit93RyMu7RrLRM= 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | gemspec 5 | gem 'rspec' 6 | gem 'bundler', '~> 2.1' 7 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | what3words (3.4.0) 5 | rest-client (>= 1.8, < 3.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | addressable (2.8.7) 11 | public_suffix (>= 2.0.2, < 7.0) 12 | ast (2.4.2) 13 | bigdecimal (3.1.8) 14 | crack (1.0.0) 15 | bigdecimal 16 | rexml 17 | diff-lcs (1.5.1) 18 | domain_name (0.5.20190701) 19 | unf (>= 0.0.5, < 1.0.0) 20 | hashdiff (1.1.0) 21 | http-accept (1.7.0) 22 | http-cookie (1.0.6) 23 | domain_name (~> 0.5) 24 | mime-types (3.5.2) 25 | mime-types-data (~> 3.2015) 26 | mime-types-data (3.2024.0702) 27 | netrc (0.11.0) 28 | parallel (1.24.0) 29 | parser (2.7.2.0) 30 | ast (~> 2.4.1) 31 | powerpack (0.1.3) 32 | public_suffix (5.1.1) 33 | rainbow (2.2.2) 34 | rake 35 | rake (12.3.3) 36 | rest-client (2.1.0) 37 | http-accept (>= 1.7.0, < 2.0) 38 | http-cookie (>= 1.0.2, < 2.0) 39 | mime-types (>= 1.16, < 4.0) 40 | netrc (~> 0.8) 41 | rexml (3.3.6) 42 | strscan 43 | rspec (3.13.0) 44 | rspec-core (~> 3.13.0) 45 | rspec-expectations (~> 3.13.0) 46 | rspec-mocks (~> 3.13.0) 47 | rspec-core (3.13.0) 48 | rspec-support (~> 3.13.0) 49 | rspec-expectations (3.13.1) 50 | diff-lcs (>= 1.2.0, < 2.0) 51 | rspec-support (~> 3.13.0) 52 | rspec-mocks (3.13.1) 53 | diff-lcs (>= 1.2.0, < 2.0) 54 | rspec-support (~> 3.13.0) 55 | rspec-support (3.13.1) 56 | rubocop (0.49.0) 57 | parallel (~> 1.10) 58 | parser (>= 2.3.3.1, < 3.0) 59 | powerpack (~> 0.1) 60 | rainbow (>= 1.99.1, < 3.0) 61 | ruby-progressbar (~> 1.7) 62 | unicode-display_width (~> 1.0, >= 1.0.1) 63 | ruby-progressbar (1.13.0) 64 | strscan (3.1.0) 65 | unf (0.1.4) 66 | unf_ext 67 | unf_ext (0.0.9.1) 68 | unicode-display_width (1.8.0) 69 | webmock (3.23.1) 70 | addressable (>= 2.8.0) 71 | crack (>= 0.3.2) 72 | hashdiff (>= 0.4.0, < 2.0.0) 73 | 74 | PLATFORMS 75 | universal-darwin-23 76 | x86_64-linux 77 | 78 | DEPENDENCIES 79 | bundler (~> 2.1) 80 | rake (~> 12.3) 81 | rspec 82 | rubocop (~> 0.49.0) 83 | webmock (~> 3.0) 84 | what3words! 85 | 86 | BUNDLED WITH 87 | 2.3.16 88 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, 2017 What3Words 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. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # what3words what3words Ruby wrapper 2 | 3 | [![Build Status](https://travis-ci.org/what3words/w3w-ruby-wrapper.svg?branch=master)](https://travis-ci.org/what3words/w3w-ruby-wrapper) 4 | 5 | The Ruby wrapper is useful for Ruby developers who wish to seamlessly integrate the [what3words Public API](https://developer.what3words.com/public-api) into their Ruby applications, without the hassle of having to manage the low level API calls themselves. 6 | 7 | The what3words API is a fast, simple interface which allows you to convert what3words addresses such as `///index.home.raft` to latitude and longitude coordinates such as `-0.203586, 51.521251` and vice versa. It features a powerful autosuggest function, which can validate and autocorrect user input and limit it to certain geographic areas (this powers the search box on our map site). It allows you to request a section of the what3words grid (which can be requested as GeoJSON for easy display on online maps), and to request the list of all languages supported by what3words. For advanced users, autosuggest can be used to post-process voice output. 8 | 9 | All coordinates are latitude,longitude pairs in standard `WGS-84` (as commonly used worldwide in GPS systems). All latitudes must be in the range of `-90 to 90 (inclusive)`. 10 | 11 | ## Installation 12 | 13 | The library is available through [RubyGems](https://rubygems.org/gems/what3words). 14 | 15 | You can simply add this line to your application's Gemfile: 16 | 17 | ``` 18 | gem 'what3words', '~> 3.4' 19 | ``` 20 | 21 | And then execute: 22 | 23 | ```shell 24 | $ bundle 25 | ``` 26 | 27 | Or install it yourself as: 28 | 29 | ```shell 30 | $ gem install what3words 31 | ``` 32 | 33 | ## Usage 34 | 35 | Sign up for an API key at [https://developer.what3words.com](https://developer.what3words.com) 36 | 37 | See [https://developer.what3words.com/public-api/docs](https://developer.what3words.com/public-api/docs) for all parameters that can be passed to the API calls. 38 | 39 | If not using Bundler, require it: 40 | 41 | ```ruby 42 | require 'what3words' 43 | ``` 44 | 45 | Then: 46 | 47 | ```ruby 48 | what3words = What3Words::API.new(:key => "YOURAPIKEY") 49 | ``` 50 | 51 | Convert to Coordinates: convert a what3words address into GPS coordinates (WGS84) 52 | 53 | ```ruby 54 | what3words.convert_to_coordinates 'prom.cape.pump' 55 | ``` 56 | 57 | **Expected Output** 58 | ``` 59 | # => {:country=>"GB", :square=>{:southwest=>{:lng=>-0.195426, :lat=>51.484449}, :northeast=>{:lng=>-0.195383, :lat=>51.484476}}, :nearestPlace=>"Kensington, London", :coordinates=>{:lng=>-0.195405, :lat=>51.484463}, :words=>"prom.cape.pump", :language=>"en", :map=>"https://w3w.co/prom.cape.pump"} 60 | ``` 61 | 62 | ## API 63 | ### Convert to Coordinates 64 | Convert a what3words address into GPS coordinates and return what3words for the same position. 65 | 66 | ```ruby 67 | what3words.convert_to_coordinates "prom.cape.pump" 68 | ``` 69 | 70 | **Expected Output** 71 | ``` 72 | # => {:country=>"GB", :square=>{:southwest=>{:lng=>-0.195426, :lat=>51.484449}, :northeast=>{:lng=>-0.195383, :lat=>51.484476}}, :nearestPlace=>"Kensington, London", :coordinates=>{:lng=>-0.195405, :lat=>51.484463}, :words=>"prom.cape.pump", :language=>"en", :map=>"https://w3w.co/prom.cape.pump"} 73 | ``` 74 | Supported keyword params for `convert_to_coordinates` call: 75 | 76 | * `words` A what3words address as a string 77 | * `format` Return data format type. It can be one of json (the default) or geojson 78 | 79 | 80 | ### Convert to 3WA 81 | Convert position information, latitude and longitude coordinates, into a what3words address. 82 | 83 | ```ruby 84 | what3words.convert_to_3wa [29.567041, 106.587875] 85 | ``` 86 | 87 | **Expected Output** 88 | ``` 89 | # => {:country=>"CN", :square=>{:southwest=>{:lng=>106.58786, :lat=>29.567028}, :northeast=>{:lng=>106.587891, :lat=>29.567055}}, :nearestPlace=>"Chongqing", :coordinates=>{:lng=>106.587875, :lat=>29.567041}, :words=>"disclose.strain.redefined", :language=>"en", :map=>"https://w3w.co/disclose.strain.redefined"} 90 | ``` 91 | 92 | Convert position information to a what3words address in a specific language 93 | 94 | ```ruby 95 | what3words.convert_to_3wa [29.567041, 106.587875], language: 'fr' 96 | ``` 97 | 98 | **Expected Output** 99 | ``` 100 | # => :country=>"CN", :square=>{:southwest=>{:lng=>106.58786, :lat=>29.567028}, :northeast=>{:lng=>106.587891, :lat=>29.567055}}, :nearestPlace=>"Chongqing", :coordinates=>{:lng=>106.587875, :lat=>29.567041}, :words=>"courgette.rabotons.infrason", :language=>"fr", :map=>"https://w3w.co/courgette.rabotons.infrason"} 101 | ``` 102 | 103 | Supported keyword params for `convert_to_3wa` call: 104 | 105 | * `coordinates` The coordinates of the location to convert to what3words address 106 | * `language` (defaults to en) - A supported what3words address language as an ISO 639-1 2 letter code 107 | * `format` Return data format type. It can be one of json (the default) or geojson 108 | 109 | ### Autosuggest 110 | Returns a list of what3words addresses based on user input and other parameters. 111 | 112 | This resource provides corrections for the following types of input error: 113 | - typing errors 114 | - spelling errors 115 | - misremembered words (e.g. singular vs. plural) 116 | - words in the wrong order 117 | 118 | The autosuggest resource determines possible corrections to the supplied what3words address string based on the probability of the input errors listed above and returns a ranked list of suggestions. This resource can also take into consideration the geographic proximity of possible corrections to a given location to further improve the suggestions returned. 119 | 120 | See [https://developer.what3words.com/public-api/docs#autosuggest](https://developer.what3words.com/public-api/docs#autosuggest) for detailed information 121 | 122 | Gets suggestions in french for this address: 123 | 124 | ```ruby 125 | what3words.autosuggest 'trop.caler.perdre', language: 'fr' 126 | ``` 127 | 128 | **Expected Output** 129 | ``` 130 | # => {:suggestions=>[{:country=>"FR", :nearestPlace=>"Saint-Lary-Soulan, Hautes-Pyrénées", :words=>"trier.caler.perdre", :rank=>1, :language=>"fr"}, {:country=>"ET", :nearestPlace=>"Asbe Teferi, Oromiya", :words=>"trôler.caler.perdre", :rank=>2, :language=>"fr"}, {:country=>"CN", :nearestPlace=>"Ulanhot, Inner Mongolia", :words=>"froc.caler.perdre", :rank=>3, :language=>"fr"}]} 131 | ``` 132 | 133 | Gets suggestions for a different number of suggestions, i.e. 10 for this address: 134 | 135 | ```ruby 136 | what3words.autosuggest 'disclose.strain.redefin', language: 'en', 'n-results': 10 137 | ``` 138 | 139 | **Expected Output** 140 | ``` 141 | # => {:suggestions=>[{:country=>"SO", :nearestPlace=>"Jamaame, Lower Juba", :words=>"disclose.strain.redefine", :rank=>1, :language=>"en"}, {:country=>"ZW", :nearestPlace=>"Mutoko, Mashonaland East", :words=>"discloses.strain.redefine", :rank=>2, :language=>"en"}, {:country=>"MM", :nearestPlace=>"Mogok, Mandalay", :words=>"disclose.strains.redefine", :rank=>3, :language=>"en"}, {:country=>"CN", :nearestPlace=>"Chongqing", :words=>"disclose.strain.redefined", :rank=>4, :language=>"en"}, {:country=>"ZM", :nearestPlace=>"Binga, Matabeleland North", :words=>"disclosing.strain.redefine", :rank=>5, :language=>"en"}, {:country=>"XH", :nearestPlace=>"Leh, Ladakh", :words=>"disclose.straining.redefine", :rank=>6, :language=>"en"}, {:country=>"US", :nearestPlace=>"Kamas, Utah", :words=>"disclose.strain.redefining", :rank=>7, :language=>"en"}, {:country=>"GN", :nearestPlace=>"Boké", :words=>"disclose.strained.redefine", :rank=>8, :language=>"en"}, {:country=>"BO", :nearestPlace=>"Pailón, Santa Cruz", :words=>"discloses.strains.redefine", :rank=>9, :language=>"en"}, {:country=>"US", :nearestPlace=>"McGrath, Alaska", :words=>"discloses.strain.redefined", :rank=>10, :language=>"en"}]} 142 | ``` 143 | 144 | Gets suggestions when the coordinates for focus has been provided for this address: 145 | 146 | ```ruby 147 | what3words.autosuggest 'filled.count.soap', focus: [51.4243877,-0.34745] 148 | ``` 149 | 150 | **Expected Output** 151 | ``` 152 | # => {:suggestions=>[{:country=>"US", :nearestPlace=>"Homer, Alaska", :words=>"fund.with.code", :rank=>1, :language=>"en"}, {:country=>"AU", :nearestPlace=>"Kumpupintil, Western Australia", :words=>"funk.with.code", :rank=>2, :language=>"en"}, {:country=>"US", :nearestPlace=>"Charleston, West Virginia", :words=>"fund.with.cove", :rank=>3, :language=>"en"}]} 153 | ``` 154 | 155 | Gets suggestions for a different number of focus results for this address: 156 | 157 | ```ruby 158 | what3words.autosuggest 'disclose.strain.redefin', language: 'en', 'n-focus-results': 3 159 | ``` 160 | 161 | **Expected Output** 162 | ``` 163 | # => {:suggestions=>[{:country=>"SO", :nearestPlace=>"Jamaame, Lower Juba", :words=>"disclose.strain.redefine", :rank=>1, :language=>"en"}, {:country=>"ZW", :nearestPlace=>"Mutoko, Mashonaland East", :words=>"discloses.strain.redefine", :rank=>2, :language=>"en"}, {:country=>"MM", :nearestPlace=>"Mogok, Mandalay", :words=>"disclose.strains.redefine", :rank=>3, :language=>"en"}]} 164 | ``` 165 | 166 | Gets suggestions for a voice input type mode, i.e. generic-voice, for this address: 167 | 168 | ```ruby 169 | what3words.autosuggest 'fun with code', 'input-type': 'generic-voice', language: 'en' 170 | ``` 171 | 172 | **Expected Output** 173 | ``` 174 | # => {:suggestions=>[{:country=>"US", :nearestPlace=>"Homer, Alaska", :words=>"fund.with.code", :rank=>1, :language=>"en"}, {:country=>"AU", :nearestPlace=>"Kumpupintil, Western Australia", :words=>"funk.with.code", :rank=>2, :language=>"en"}, {:country=>"US", :nearestPlace=>"Charleston, West Virginia", :words=>"fund.with.cove", :rank=>3, :language=>"en"}]} 175 | ``` 176 | 177 | Gets suggestions for a restricted area by clipping to country for this address: 178 | 179 | ```ruby 180 | what3words.autosuggest 'disclose.strain.redefin', 'clip-to-country': 'GB,BE' 181 | ``` 182 | 183 | **Expected Output** 184 | ``` 185 | # => {:suggestions=>[{:country=>"GB", :nearestPlace=>"Nether Stowey, Somerset", :words=>"disclose.retrain.redefined", :rank=>1, :language=>"en"}, {:country=>"BE", :nearestPlace=>"Zemst, Flanders", :words=>"disclose.strain.reckon", :rank=>2, :language=>"en"}, {:country=>"GB", :nearestPlace=>"Waddington, Lincolnshire", :words=>"discloses.trains.redefined", :rank=>3, :language=>"en"}]} 186 | ``` 187 | 188 | Gets suggestions for a restricted area by clipping to a bounding-box for this address: 189 | 190 | ```ruby 191 | what3words.autosuggest 'disclose.strain.redefin', 'clip-to-bounding-box': [51.521, -0.343, 52.6, 2.3324] 192 | ``` 193 | 194 | **Expected Output** 195 | ``` 196 | # => {:suggestions=>[{:country=>"GB", :nearestPlace=>"Saxmundham, Suffolk", :words=>"discloses.strain.reddish", :rank=>1, :language=>"en"}]} 197 | ``` 198 | 199 | 200 | Gets suggestions for a restricted area by clipping to a circle in km for this address: 201 | 202 | ```ruby 203 | what3words.autosuggest 'disclose.strain.redefin', 'clip-to-circle': [51.521, -0.343, 142] 204 | ``` 205 | 206 | **Expected Output** 207 | ``` 208 | # => {:suggestions=>[{:country=>"GB", :nearestPlace=>"Market Harborough, Leicestershire", :words=>"discloses.strain.reduce", :rank=>1, :language=>"en"}]} 209 | ``` 210 | 211 | Gets suggestions for a restricted area by clipping to a polygon for this address: 212 | 213 | ```ruby 214 | what3words.autosuggest 'disclose.strain.redefin', 'clip-to-polygon': [51.521, -0.343, 52.6, 2.3324, 54.234, 8.343, 51.521, -0.343] 215 | ``` 216 | 217 | **Expected Output** 218 | ``` 219 | # => {:suggestions=>[{:country=>"GB", :nearestPlace=>"Saxmundham, Suffolk", :words=>"discloses.strain.reddish", :rank=>1, :language=>"en"}]} 220 | ``` 221 | 222 | Gets suggestions for a restricted area by clipping to a polygon for this address: 223 | 224 | ```ruby 225 | what3words.w3w.autosuggest 'disclose.strain.redefin', 'prefer-land': false, 'n-results': 10 226 | ``` 227 | 228 | **Expected Output** 229 | ``` 230 | # => {:suggestions=>[{:country=>"SO", :nearestPlace=>"Jamaame, Lower Juba", :words=>"disclose.strain.redefine", :rank=>1, :language=>"en"}, {:country=>"ZW", :nearestPlace=>"Mutoko, Mashonaland East", :words=>"discloses.strain.redefine", :rank=>2, :language=>"en"}, {:country=>"MM", :nearestPlace=>"Mogok, Mandalay", :words=>"disclose.strains.redefine", :rank=>3, :language=>"en"}, {:country=>"CN", :nearestPlace=>"Chongqing", :words=>"disclose.strain.redefined", :rank=>4, :language=>"en"}, {:country=>"ZM", :nearestPlace=>"Binga, Matabeleland North", :words=>"disclosing.strain.redefine", :rank=>5, :language=>"en"}, {:country=>"XH", :nearestPlace=>"Leh, Ladakh", :words=>"disclose.straining.redefine", :rank=>6, :language=>"en"}, {:country=>"US", :nearestPlace=>"Kamas, Utah", :words=>"disclose.strain.redefining", :rank=>7, :language=>"en"}, {:country=>"GN", :nearestPlace=>"Boké", :words=>"disclose.strained.redefine", :rank=>8, :language=>"en"}, {:country=>"BO", :nearestPlace=>"Pailón, Santa Cruz", :words=>"discloses.strains.redefine", :rank=>9, :language=>"en"}, {:country=>"US", :nearestPlace=>"McGrath, Alaska", :words=>"discloses.strain.redefined", :rank=>10, :language=>"en"}]} 231 | ``` 232 | 233 | Supported keyword params for `autosuggest` call: 234 | * `input` The full or partial what3words address to obtain suggestions for. At minimum this must be the first two complete words plus at least one character from the third word. 235 | * `language` A supported what3words address language as an ISO 639-1 2 letter code. This setting is on by default. Use false to disable this setting and receive more suggestions in the sea. 236 | * `n_results` The number of AutoSuggest results to return. A maximum of 100 results can be specified, if a number greater than this is requested, this will be truncated to the maximum. The default is 3. 237 | * `n_focus_results` Specifies the number of results (must be <= n_results) within the results set which will have a focus. Defaults to n_results. This allows you to run autosuggest with a mix of focussed and unfocussed results, to give you a "blend" of the two. 238 | * `clip-to-country` Restricts autosuggest to only return results inside the countries specified by comma-separated list of uppercase ISO 3166-1 alpha-2 country codes (for example, to restrict to Belgium and the UK, use clip_to_country="GB,BE"). 239 | * `clip-to-bounding-box` Restrict autosuggest results to a bounding box, specified by coordinates. 240 | * `clip-to-circle` Restrict autosuggest results to a circle, specified by the center of the circle, latitude and longitude, and a distance in kilometres which represents the radius. For convenience, longitude is allowed to wrap around 180 degrees. For example 181 is equivalent to -179. 241 | * `clip-to-polygon` Restrict autosuggest results to a polygon, specified by a list of coordinates. The polygon should be closed, i.e. the first element should be repeated as the last element; also the list should contain at least 4 entries. The API is currently limited to accepting up to 25 pairs. 242 | * `input-type` For power users, used to specify voice input mode. Can be text (default), vocon-hybrid, nmdp-asr or generic-voice. 243 | * `prefer-land` Makes autosuggest prefer results on land to those in the sea. 244 | 245 | ### Grid 246 | Returns a section of the 3m x 3m what3words grid for a given area. 247 | 248 | See [https://developer.what3words.com/public-api/docs#grid-section](https://developer.what3words.com/public-api/docs#grid-section) for detailed information. 249 | 250 | Gets grid for these bounding box northeast 52.208867,0.117540,52.207988,0.116126. 251 | 252 | ```ruby 253 | what3words.grid_section '52.208867,0.117540,52.207988,0.116126' 254 | ``` 255 | 256 | **Expected Output** 257 | ``` 258 | # => {:lines=>[{:start=>{:lng=>0.116126, :lat=>52.20801}, :end=>{:lng=>0.11754, :lat=>52.20801}}, {:start=>{:lng=>0.116126, :lat=>52.208037}, :end=>{:lng=>0.11754, :lat=>52.208037}}, {:start=>{:lng=>0.116126, :lat=>52.208064}, :end=>{:lng=>0.11754, :lat=>52.208064}}, ___...___ ]} 259 | ``` 260 | 261 | Supported keyword params for `grid_section` call: 262 | * `bounding-box` The bounding box is specified by the northeast and southwest corner coordinates, for which the grid should be returned 263 | * `format` Return data format type. It can be one of json (the default) or geojson 264 | 265 | ### Get Languages 266 | Retrieve a list of available what3words languages. 267 | 268 | ```ruby 269 | what3words.available_languages 270 | ``` 271 | 272 | **Expected Output** 273 | ``` 274 | # => {:languages=>[{:nativeName=>"Deutsch", :code=>"de", :name=>"German"}, {:nativeName=>"हिन्दी", :code=>"hi", :name=>"Hindi"}, {:nativeName=>"Português", :code=>"pt", :name=>"Portuguese"}, {:nativeName=>"Magyar", :code=>"hu", :name=>"Hungarian"}, {:nativeName=>"Українська", :code=>"uk", :name=>"Ukrainian"}, {:nativeName=>"Bahasa Indonesia", :code=>"id", :name=>"Bahasa Indonesia"}, {:nativeName=>"اردو", :code=>"ur", :name=>"Urdu"}, ___...___]} 275 | ``` 276 | 277 | See [https://developer.what3words.com/public-api/docs#available-languages](https://developer.what3words.com/public-api/docs#available-languages) for the original API call documentation. 278 | 279 | ### RegEx functions 280 | 281 | This section introduces RegEx functions that can assist with checking and finding possible what3words addresses in strings. The three main functions covered are: 282 | 283 | `isPossible3wa` – Match what3words address format; 284 | `findPossible3wa` – Find what3words address in Text; 285 | `isValid3wa` – Verify a what3words address with the API; 286 | 287 | #### isPossible3wa 288 | 289 | Our API wrapper RegEx function `isPossible3wa` can be used used to detect if a text string (like `filled.count.soap`) in the format of a what3words address without having to ask the API. This functionality checks if a given string could be a what3words address. It returns true if it could be, otherwise false. 290 | 291 | **Note**: This function checks the text format but not the validity of a what3words address. Use `isValid3wa` to verify validity. 292 | 293 | ```ruby 294 | require 'what3words' 295 | 296 | def main 297 | # Initialize the What3Words API with your API key 298 | api_key = 'YOUR_API_KEY' 299 | w3w = What3Words::API.new(:key => api_key) 300 | 301 | # Example what3words addresses 302 | addresses = ["filled.count.soap", "not a 3wa", "not.3wa address"] 303 | 304 | # Check if the addresses are possible what3words addresses 305 | addresses.each do |address| 306 | is_possible = w3w.isPossible3wa(address) 307 | puts "Is '#{address}' a possible what3words address? #{is_possible}" 308 | end 309 | end 310 | 311 | if __FILE__ == $0 312 | main 313 | end 314 | ``` 315 | 316 | **Expected Output** 317 | 318 | isPossible3wa(“filled.count.soap”) returns true 319 | isPossible3wa(“not a 3wa”) returns false 320 | isPossible3wa(“not.3wa address”)returns false 321 | 322 | #### findPossible3wa 323 | 324 | Our API wrapper RegEx function `findPossible3wa` can be used to detect a what3words address within a block of text, useful for finding a what3words address in fields like Delivery Notes. For example, it can locate a what3words address in a note like “Leave at my front door ///filled.count.soap”. The function will match if there is a what3words address within the text. If no possible addresses are found, it returns an empty list. 325 | 326 | **Note**: 327 | 328 | - This function checks the text format but not the validity of a what3words address. Use `isValid3wa` to verify validity. 329 | - This function is designed to work across languages but do not work for `Vietnamese (VI)` due to spaces within words. 330 | 331 | ```ruby 332 | require 'what3words' 333 | 334 | def main 335 | # Initialize the what3words API with your API key 336 | api_key = 'YOUR_API_KEY' 337 | w3w = What3Words::API.new(:key => api_key) 338 | 339 | # Example texts 340 | texts = [ 341 | "Please leave by my porch at filled.count.soap", 342 | "Please leave by my porch at filled.count.soap or deed.tulip.judge", 343 | "Please leave by my porch at" 344 | ] 345 | 346 | # Check if the texts contain possible what3words addresses 347 | texts.each do |text| 348 | possible_addresses = w3w.findPossible3wa(text) 349 | puts "Possible what3words addresses in '#{text}': #{possible_addresses}" 350 | end 351 | end 352 | 353 | if __FILE__ == $0 354 | main 355 | end 356 | ``` 357 | 358 | **Expected Output** 359 | 360 | findPossible3wa(“Please leave by my porch at filled.count.soap”) returns ['filled.count.soap'] 361 | findPossible3wa(“Please leave by my porch at filled.count.soap or deed.tulip.judge”) returns ['filled.count.soap', 'deed.tulip.judge'] 362 | findPossible3wa(“Please leave by my porch at”) returns [] 363 | 364 | #### isValid3wa 365 | 366 | Our API wrapper RegEx function `isValid3wa` can be used to determine if a string is a valid what3words address by checking it against the what3words RegEx filter and verifying it with the what3words API. 367 | 368 | ```ruby 369 | require 'what3words' 370 | 371 | def main 372 | # Initialize the what3words API with your API key 373 | api_key = 'YOUR_API_KEY' 374 | w3w = What3Words::API.new(:key => api_key) 375 | 376 | # Example addresses 377 | addresses = [ 378 | "filled.count.soap", 379 | "filled.count.", 380 | "coding.is.cool" 381 | ] 382 | 383 | # Check if the addresses are valid what3words addresses 384 | addresses.each do |address| 385 | is_valid = w3w.isValid3wa(address) 386 | puts "Is '#{address}' a valid what3words address? #{is_valid}" 387 | end 388 | end 389 | 390 | if __FILE__ == $0 391 | main 392 | end 393 | ``` 394 | **Expected Outputs** 395 | 396 | isValid3wa(“filled.count.soap”) returns True 397 | isValid3wa(“filled.count.”) returns False 398 | isValid3wa(“coding.is.cool”) returns False 399 | 400 | Also make sure to replace `` with your actual API key. These functionalities provide different levels of validation for what3words addresses, from simply identifying potential addresses to verifying their existence on Earth. 401 | 402 | 403 | See [https://developer.what3words.com/tutorial/ruby#regex-functions](https://developer.what3words.com/tutorial/ruby#regex-functions) for further documentation. 404 | 405 | 406 | ## Testing 407 | 408 | * Prerequisite : we are using [bundler](https://rubygems.org/gems/bundler) `$ gem install bundler` 409 | 410 | * W3W-API-KEY: For safe storage of your API key on your computer, you can define that API key using your system’s environment variables. 411 | ```bash 412 | $ export W3W_API_KEY= 413 | ``` 414 | 415 | * on your cloned folder 416 | 1. `$ cd w3w-ruby-wrapper` 417 | 1. `$ bundle update` 418 | 1. `$ rake rubocop spec` 419 | 420 | To run the tests, type on your terminal: 421 | ```bash 422 | $ bundle exec rspec 423 | ``` 424 | 425 | ## Issues 426 | 427 | Find a bug or want to request a new feature? Please let us know by submitting an issue. 428 | 429 | ## Contributing 430 | Anyone and everyone is welcome to contribute. 431 | 432 | 1. Fork it (http://github.com/what3words/w3w-ruby-wrapper and click "Fork") 433 | 1. Create your feature branch (`git checkout -b my-new-feature`) 434 | 1. Commit your changes (`git commit -am 'Add some feature'`) 435 | 1. Don't forget to update README and bump [version](./lib/what3words/version.rb) using [semver](https://semver.org/) 436 | 1. Push to the branch (`git push origin my-new-feature`) 437 | 1. Create new Pull Request 438 | 439 | # Revision History 440 | 441 | * `v3.4.0` 15/01/25 - Update dependencies and upgrade ruby gemspec 442 | * `v3.3.0` 12/08/24 - Update error message to handle c2c calls and dependencies 443 | * `v3.2.0` 17/07/24 - Update regex patterns 444 | * `v3.1.0` 16/07/24 - Update tests and code to host the regex functions 445 | * `v3.0.0` 12/05/22 - Update endpoints and tests to API v3, added HTTP headers 446 | * `v2.2.0` 03/01/18 - Enforce Ruby 2.4 Support - Thanks to PR from Dimitrios Zorbas [@Zorbash](https://github.com/zorbash) 447 | * `v2.1.1` 22/05/17 - Update gemspec to use rubocop 0.48.1, and fixes spec accordingly 448 | * `v2.1.0` 28/03/17 - Added multilingual version of `autosuggest` and `standardblend` 449 | * `v2.0.4` 27/03/17 - Updated README with `languages` method result updated from live result 450 | * `v2.0.3` 24/10/16 - Fixed `display` in `assemble_common_request_params` 451 | * `v2.0.2` 10/06/16 - Added travis-ci builds 452 | * `v2.0.0` 10/06/16 - Updated wrapper to use what3words API v2 453 | 454 | ## Licensing 455 | 456 | The MIT License (MIT) 457 | 458 | A copy of the license is available in the repository's [license](LICENSE.txt) file. 459 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | 5 | begin 6 | require 'rubocop/rake_task' 7 | RuboCop::RakeTask.new(:rubocop) do |t| 8 | t.options = ['--display-cop-names'] 9 | end 10 | require 'rspec/core/rake_task' 11 | RSpec::Core::RakeTask.new(:spec) 12 | rescue LoadError => e 13 | print e 14 | end 15 | -------------------------------------------------------------------------------- /lib/what3words.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module What3Words # :nodoc: don't document this 4 | end 5 | 6 | require 'what3words/api' 7 | -------------------------------------------------------------------------------- /lib/what3words/api.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rest-client' 4 | require 'json' 5 | require File.expand_path('../version', __FILE__) 6 | require 'what3words/version' 7 | 8 | module What3Words 9 | # What3Words v3 API wrapper 10 | class API 11 | class Error < RuntimeError; end 12 | # class ResponseError < Error; end 13 | class ResponseError < StandardError; end 14 | class WordError < Error; end 15 | 16 | REGEX_3_WORD_ADDRESS = /^\/*(?:[^0-9`~!@#$%^&*()+\-_=\[\{\]}\\|'<>.,?\/\";:£§º©®\s]{1,}[.。。・・︒។։။۔።।][^0-9`~!@#$%^&*()+\-_=\[\{\]}\\|'<>.,?\/\";:£§º©®\s]{1,}[.。。・・︒។։။۔።।][^0-9`~!@#$%^&*()+\-_=\[\{\]}\\|'<>.,?\/\";:£§º©®\s]{1,}|[<.,>?\/\";:£§º©®\s]+[.。。・・︒។։။۔።।][^0-9`~!@#$%^&*()+\-_=\[\{\]}\\|'<>.,?\/\";:£§º©®\s]+|[^0-9`~!@#$%^&*()+\-_=\[\{\]}\\|'<>.,?\/\";:£§º©®\s]+([\u0020\u00A0][^0-9`~!@#$%^&*()+\-_=\[\{\]}\\|'<>.,?\/\";:£§º©®\s]+){1,3}[.。。・・︒។։။۔።।][^0-9`~!@#$%^&*()+\-_=\[\{\]}\\|'<>.,?\/\";:£§º©®\s]+([\u0020\u00A0][^0-9`~!@#$%^&*()+\-_=\[\{\]}\\|'<>.,?\/\";:£§º©®\s]+){1,3}[.。。・・︒។։။۔።।][^0-9`~!@#$%^&*()+\-_=\[\{\]}\\|'<>.,?\/\";:£§º©®\s]+([\u0020\u00A0][^0-9`~!@#$%^&*()+\-_=\[\{\]}\\|'<>.,?\/\";:£§º©®\s]+){1,3})$/u.freeze 17 | BASE_URL = 'https://api.what3words.com/v3/' 18 | 19 | ENDPOINTS = { 20 | convert_to_coordinates: 'convert-to-coordinates', 21 | convert_to_3wa: 'convert-to-3wa', 22 | available_languages: 'available-languages', 23 | autosuggest: 'autosuggest', 24 | grid_section: 'grid-section' 25 | }.freeze 26 | 27 | WRAPPER_VERSION = What3Words::VERSION 28 | 29 | def initialize(params) 30 | @key = params.fetch(:key) 31 | end 32 | 33 | attr_reader :key 34 | 35 | def convert_to_coordinates(words, params = {}) 36 | """ 37 | Take a 3 word address and turn it into a pair of coordinates. 38 | 39 | Params 40 | ------ 41 | :param string words: A 3 word address as a string 42 | :param string format: Return data format type; can be one of json (the default), geojson 43 | :rtype: Hash 44 | """ 45 | words_string = get_words_string(words) 46 | request_params = assemble_convert_to_coordinates_request_params(words_string, params) 47 | request!(:convert_to_coordinates, request_params) 48 | end 49 | 50 | def convert_to_3wa(position, params = {}) 51 | """ 52 | Take latitude and longitude coordinates and turn them into a 3 word address. 53 | 54 | Params 55 | ------ 56 | :param array position: The coordinates of the location to convert to 3 word address 57 | :param string format: Return data format type; can be one of json (the default), geojson 58 | :param string language: A supported 3 word address language as an ISO 639-1 2 letter code. 59 | :rtype: Hash 60 | """ 61 | request_params = assemble_convert_to_3wa_request_params(position, params) 62 | request!(:convert_to_3wa, request_params) 63 | end 64 | 65 | def grid_section(bbox, params = {}) 66 | """ 67 | Returns a section of the 3m x 3m what3words grid for a given area. 68 | 69 | Params 70 | ------ 71 | :param string bbox: Bounding box, specified by the northeast and southwest corner coordinates, 72 | :param string format: Return data format type; can be one of json (the default), geojson 73 | :rtype: Hash 74 | """ 75 | request_params = assemble_grid_request_params(bbox, params) 76 | request!(:grid_section, request_params) 77 | end 78 | 79 | def available_languages 80 | """ 81 | Retrieve a list of available 3 word languages. 82 | 83 | :rtype: Hash 84 | """ 85 | request_params = assemble_common_request_params({}) 86 | request!(:available_languages, request_params) 87 | end 88 | 89 | def autosuggest(input, params = {}) 90 | """ 91 | Returns a list of 3 word addresses based on user input and other parameters. 92 | 93 | Params 94 | ------ 95 | :param string input: The full or partial 3 word address to obtain suggestions for. 96 | :param int n_results: The number of AutoSuggest results to return. 97 | :param array focus: A location, specified as a latitude,longitude used to refine the results. 98 | :param int n_focus_results: Specifies the number of results (must be <= n_results) within the results set which will have a focus. 99 | :param string clip_to_country: Restricts autosuggest to only return results inside the countries specified by comma-separated list of uppercase ISO 3166-1 alpha-2 country codes. 100 | :param array clip_to_bounding_box: Restrict autosuggest results to a bounding box, specified by coordinates. 101 | :param array clip_to_circle: Restrict autosuggest results to a circle, specified by the center of the circle, latitude and longitude, and a distance in kilometres which represents the radius. 102 | :param array clip_to_polygon: Restrict autosuggest results to a polygon, specified by a list of coordinates. 103 | :param string input_type: For power users, used to specify voice input mode. Can be text (default), vocon-hybrid, nmdp-asr or generic-voice. 104 | :param string prefer_land: Makes autosuggest prefer results on land to those in the sea. 105 | :param string language: A supported 3 word address language as an ISO 639-1 2 letter code. 106 | :rtype: Hash 107 | """ 108 | request_params = assemble_autosuggest_request_params(input, params) 109 | request!(:autosuggest, request_params) 110 | end 111 | 112 | def isPossible3wa(text) 113 | """ 114 | Determines if the string passed in is the form of a three word address. 115 | This does not validate whether it is a real address as it returns true for x.x.x 116 | 117 | Params 118 | ------ 119 | :param string text: text to check 120 | :rtype: Boolean 121 | """ 122 | regex_match = REGEX_3_WORD_ADDRESS 123 | !(text.match(regex_match).nil?) 124 | end 125 | 126 | def findPossible3wa(text) 127 | """ 128 | Searches the string passed in for all substrings in the form of a three word address. 129 | This does not validate whether it is a real address as it will return x.x.x as a result 130 | 131 | Params 132 | ------ 133 | :param string text: text to check 134 | :rtype: Array 135 | """ 136 | regex_search = /[^\d`~!@#$%^&*()+\-=\[\]{}\\|'<>.,?\/\";:£§º©®\s]{1,}[.。。・・︒។։။۔።।][^\d`~!@#$%^&*()+\-=\[\]{}\\|'<>.,?\/\";:£§º©®\s]{1,}[.。。・・︒។։။۔።।][^\d`~!@#$%^&*()+\-=\[\]{}\\|'<>.,?\/\";:£§º©®\s]{1,}/u 137 | text.scan(regex_search) 138 | end 139 | 140 | def didYouMean(text) 141 | """ 142 | Determines if the string passed in is almost in the form of a three word address. 143 | This will return True for values such as 'filled-count-soap' and 'filled count soap' 144 | 145 | Params 146 | ------ 147 | :param string text: text to check 148 | :rtype: Boolean 149 | """ 150 | regex_didyoumean = /^\/?[^0-9`~!@#$%^&*()+\-=\[\{\]}\\|'<>.,?\/\";:£§º©®\s]{1,}[.\uFF61\u3002\uFF65\u30FB\uFE12\u17D4\u0964\u1362\u3002:။^_۔։ ,\\\/+'&\\:;|\u3000-]{1,2}[^0-9`~!@#$%^&*()+\-=\[\{\]}\\|'<>.,?\/\";:£§º©®\s]{1,}[.\uFF61\u3002\uFF65\u30FB\uFE12\u17D4\u0964\u1362\u3002:။^_۔։ ,\\\/+'&\\:;|\u3000-]{1,2}[^0-9`~!@#$%^&*()+\-=\[\{\]}\\|'<>.,?\/\";:£§º©®\s]{1,}$/u 151 | !(text.match(regex_didyoumean).nil?) 152 | end 153 | 154 | def isValid3wa(text) 155 | """ 156 | Determines if the string passed in is a real three word address. It calls the API 157 | to verify it refers to an actual place on earth. 158 | 159 | Params 160 | ------ 161 | :param String text: text to check 162 | 163 | :rtype: Boolean 164 | """ 165 | if isPossible3wa(text) 166 | result = autosuggest(text, 'n-results': 1) 167 | if result[:suggestions] && result[:suggestions].length > 0 168 | return result[:suggestions][0][:words] == text 169 | end 170 | end 171 | false 172 | end 173 | 174 | private 175 | 176 | def request!(endpoint_name, params) 177 | headers = { "X-W3W-Wrapper": "what3words-Ruby/#{WRAPPER_VERSION}" } 178 | response = RestClient.get(endpoint(endpoint_name), params: params, headers: headers) 179 | parsed_response = JSON.parse(response.body) 180 | 181 | if parsed_response['error'] 182 | error_code = parsed_response['error']['code'] 183 | error_message = parsed_response['error']['message'] 184 | raise ResponseError, "#{error_code}: #{error_message}" 185 | end 186 | 187 | deep_symbolize_keys(parsed_response) 188 | rescue RestClient::ExceptionWithResponse => e 189 | handle_rest_client_error(e) 190 | rescue RestClient::Exception => e 191 | raise ResponseError, "RestClient error: #{e.message}" 192 | end 193 | 194 | def handle_rest_client_error(error) 195 | response = error.response 196 | begin 197 | parsed_response = JSON.parse(response.body) 198 | rescue JSON::ParserError 199 | raise ResponseError, "Invalid JSON response: #{response}" 200 | end 201 | 202 | if parsed_response['error'] 203 | error_code = parsed_response['error']['code'] 204 | error_message = parsed_response['error']['message'] 205 | raise ResponseError, "#{error_code}: #{error_message}" 206 | else 207 | raise ResponseError, "Unknown error: #{response}" 208 | end 209 | end 210 | 211 | def get_words_string(words) 212 | words_string = words.is_a?(Array) ? words.join('.') : words.to_s 213 | check_words(words_string) 214 | words_string 215 | end 216 | 217 | def check_words(words) 218 | raise WordError, "#{words} is not a valid 3 word address" unless REGEX_3_WORD_ADDRESS.match?(words) 219 | end 220 | 221 | def assemble_common_request_params(params) 222 | { key: key }.merge(params.slice(:language, :format)) 223 | end 224 | 225 | def assemble_convert_to_coordinates_request_params(words_string, params) 226 | { words: words_string }.merge(assemble_common_request_params(params)) 227 | end 228 | 229 | def assemble_convert_to_3wa_request_params(position, params) 230 | { coordinates: position.join(',') }.merge(assemble_common_request_params(params)) 231 | end 232 | 233 | def assemble_grid_request_params(bbox, params) 234 | { 'bounding-box': bbox }.merge(assemble_common_request_params(params)) 235 | end 236 | 237 | def assemble_autosuggest_request_params(input, params) 238 | result = { input: input } 239 | result[:'n-results'] = params[:'n-results'].to_i if params[:'n-results'] 240 | result[:focus] = params[:focus].join(',') if params[:focus].respond_to?(:join) 241 | result[:'n-focus-results'] = params[:'n-focus-results'].to_i if params[:'n-focus-results'] 242 | result[:'clip-to-country'] = params[:'clip-to-country'] if params[:'clip-to-country'].respond_to?(:to_str) 243 | result[:'clip-to-bounding-box'] = params[:'clip-to-bounding-box'].join(',') if params[:'clip-to-bounding-box'].respond_to?(:join) 244 | result[:'clip-to-circle'] = params[:'clip-to-circle'].join(',') if params[:'clip-to-circle'].respond_to?(:join) 245 | result[:'clip-to-polygon'] = params[:'clip-to-polygon'].join(',') if params[:'clip-to-polygon'].respond_to?(:join) 246 | result[:'input-type'] = params[:'input-type'] if params[:'input-type'].respond_to?(:to_str) 247 | result[:'prefer-land'] = params[:'prefer-land'] if params[:'prefer-land'] 248 | result.merge(assemble_common_request_params(params)) 249 | end 250 | 251 | def deep_symbolize_keys(value) 252 | case value 253 | when Hash 254 | value.transform_keys(&:to_sym).transform_values { |v| deep_symbolize_keys(v) } 255 | when Array 256 | value.map { |v| deep_symbolize_keys(v) } 257 | else 258 | value 259 | end 260 | end 261 | 262 | def endpoint(name) 263 | BASE_URL + ENDPOINTS.fetch(name) 264 | end 265 | end 266 | end 267 | -------------------------------------------------------------------------------- /lib/what3words/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # What3Words v3 API wrapper 4 | module What3Words 5 | VERSION = '3.4.0' unless defined?(::What3Words::VERSION) 6 | end 7 | -------------------------------------------------------------------------------- /sample/sample.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | #!/usr/bin/env 3 | 4 | require 'what3words' 5 | require 'json' 6 | 7 | api_key = ENV['W3W_API_KEY'] 8 | 9 | what3words = What3Words::API.new(:key => api_key, :format => 'json') 10 | 11 | # ## convert_to_coordinates ######### 12 | res = what3words.convert_to_coordinates 'prom.cape.pump' 13 | puts '######### convert_to_coordinate #########' 14 | puts res 15 | 16 | # ## convert_to_3wa ######### 17 | res = what3words.convert_to_3wa [29.567041, 106.587875] 18 | puts '######### convert_to_3wa #########' 19 | puts res 20 | 21 | # ## grid_section ######### 22 | res = what3words.grid_section '52.208867,0.117540,52.207988,0.116126' 23 | puts '######### grid_section #########' 24 | puts res 25 | 26 | # ## available_languages ######### 27 | res = what3words.available_languages 28 | puts '######### available_languages #########' 29 | puts res 30 | 31 | 32 | # ## Vanilla autosuggest, limiting the number of results to three ######### 33 | res = what3words.autosuggest 'disclose.strain.redefin', language: 'en', 'n-results': 10 34 | puts '######### autosuggest n-results #########' 35 | puts res 36 | 37 | # ## autosuggest demonstrating clipping to polygon, circle, bounding box, and country ######### 38 | res_polygon = what3words.autosuggest 'disclose.strain.redefin', 'clip-to-polygon': [51.521, -0.343, 52.6, 2.3324, 54.234, 8.343, 51.521, -0.343] 39 | res_circle = what3words.autosuggest 'disclose.strain.redefin', 'clip-to-circle': [51.521, -0.343, 142] 40 | res_bbox = what3words.autosuggest 'disclose.strain.redefin', 'clip-to-bounding-box': [51.521, -0.343, 52.6, 2.3324] 41 | res_country = what3words.autosuggest 'disclose.strain.redefin', 'clip-to-country': 'GB,BE' 42 | puts '######### autosuggest clipping options #########' 43 | puts res_polygon 44 | puts res_circle 45 | puts res_bbox 46 | puts res_country 47 | 48 | # ## autosuggest with a focus, with that focus only applied to the first result ######### 49 | res = what3words.autosuggest 'filled.count.soap', focus: [51.4243877, -0.34745], 'n-focus-results': 3, 'n-results': 10 50 | puts '######### autosuggest with a focus ######### ' 51 | puts res 52 | 53 | # ## autosuggest with an input type of Generic Voice ######### 54 | res = what3words.autosuggest 'fun with code', 'input-type': 'generic-voice', language: 'en' 55 | puts '######### autosuggest with Generic Voice as input type ######### ' 56 | puts res 57 | 58 | # ## isPossible3wa ######### 59 | addresses = ["filled.count.soap", "not a 3wa", "not.3wa address"] 60 | addresses.each do |address| 61 | is_possible = what3words.isPossible3wa(address) 62 | puts "Is '#{address}' a possible what3words address? #{is_possible}" 63 | end 64 | 65 | # ## findPossible3wa ######### 66 | texts = [ 67 | "Please leave by my porch at filled.count.soap", 68 | "Please leave by my porch at filled.count.soap or deed.tulip.judge", 69 | "Please leave by my porch at" 70 | ] 71 | texts.each do |text| 72 | possible_addresses = what3words.findPossible3wa(text) 73 | puts "Possible what3words addresses in '#{text}': #{possible_addresses}" 74 | end 75 | 76 | # ## didYouMean ######### 77 | addresses = ["filled-count-soap", "filled count soap", "invalid#address!example", "this is not a w3w address"] 78 | addresses.each do |address| 79 | suggestion = what3words.didYouMean(address) 80 | puts "Did you mean '#{address}'? #{suggestion}" 81 | end 82 | 83 | # ## isValid3wa ######### 84 | addresses = ["filled.count.soap", "filled.count.", "coding.is.cool"] 85 | addresses.each do |address| 86 | is_valid = what3words.isValid3wa(address) 87 | puts "Is '#{address}' a valid what3words address? #{is_valid}" 88 | end 89 | 90 | -------------------------------------------------------------------------------- /spec/config.sample.yaml: -------------------------------------------------------------------------------- 1 | # config.sample.yaml 2 | -------------------------------------------------------------------------------- /spec/lib/what3words/what3words_api_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'webmock/rspec' 5 | require_relative '../../../lib/what3words/api' 6 | 7 | # to run the test type on terminal --> bundle exec rspec 8 | 9 | describe What3Words::API, 'integration', integration: true do 10 | before(:all) do 11 | WebMock.allow_net_connect! 12 | end 13 | 14 | let(:api_key) { ENV['W3W_API_KEY'] } 15 | let(:w3w) { described_class.new(key: api_key) } 16 | 17 | it 'returns errors from API with an invalid key' do 18 | badw3w = described_class.new(key: 'BADKEY') 19 | expect { badw3w.convert_to_coordinates('prom.cape.pump') } 20 | .to raise_error(described_class::ResponseError) 21 | end 22 | 23 | describe 'convert_to_coordinates' do 24 | it 'works with a valid 3 word address' do 25 | result = nil 26 | begin 27 | result = w3w.convert_to_coordinates('prom.cape.pump') 28 | rescue What3Words::API::ResponseError => e 29 | puts e.message 30 | end 31 | expect(result).to include( 32 | words: 'prom.cape.pump', 33 | language: 'en' 34 | ) if result 35 | expect(result[:coordinates]).to include( 36 | lat: 51.484463, 37 | lng: -0.195405 38 | ) if result 39 | end 40 | 41 | it 'raises a ResponseError with the correct message when quota is exceeded' do 42 | error_response = { 43 | "error": { 44 | "code": "QuotaExceeded", 45 | "message": "Quota Exceeded. Please upgrade your usage plan, or contact support@what3words.com" 46 | } 47 | }.to_json 48 | 49 | stub_request(:get, /api.what3words.com/).to_return(status: 402, body: error_response, headers: { content_type: 'application/json' }) 50 | 51 | expect { 52 | w3w.convert_to_coordinates('filled.count.soap') 53 | }.to raise_error(What3Words::API::ResponseError, 'QuotaExceeded: Quota Exceeded. Please upgrade your usage plan, or contact support@what3words.com') 54 | end 55 | 56 | it 'sends language parameter for 3 words' do 57 | result = nil 58 | begin 59 | result = w3w.convert_to_coordinates('prom.cape.pump') 60 | rescue What3Words::API::ResponseError => e 61 | puts e.message 62 | end 63 | expect(result).to include( 64 | words: 'prom.cape.pump', 65 | language: 'en' 66 | ) if result 67 | end 68 | 69 | it 'raises an error for an invalid 3 word address format' do 70 | expect { w3w.convert_to_coordinates('1.cape.pump') } 71 | .to raise_error(described_class::WordError) 72 | end 73 | 74 | it 'sends json format parameter for 3 words' do 75 | result = nil 76 | begin 77 | result = w3w.convert_to_coordinates('prom.cape.pump', format: 'json') 78 | rescue What3Words::API::ResponseError => e 79 | puts e.message 80 | end 81 | expect(result).to include( 82 | words: 'prom.cape.pump', 83 | language: 'en', 84 | country: 'GB', 85 | square: { 86 | southwest: { 87 | lng: -0.195426, 88 | lat: 51.484449 89 | }, 90 | northeast: { 91 | lng: -0.195383, 92 | lat: 51.484476 93 | } 94 | }, 95 | nearestPlace: 'Kensington, London', 96 | coordinates: { 97 | lng: -0.195405, 98 | lat: 51.484463 99 | }, 100 | map: 'https://w3w.co/prom.cape.pump' 101 | ) if result 102 | end 103 | end 104 | 105 | describe 'convert_to_3wa' do 106 | it 'converts coordinates to a 3 word address in English' do 107 | begin 108 | result = w3w.convert_to_3wa([29.567041, 106.587875], format: 'json') 109 | expect(result).to include( 110 | words: 'disclose.strain.redefined', 111 | language: 'en' 112 | ) 113 | expect(result[:coordinates]).to include( 114 | lat: 29.567041, 115 | lng: 106.587875 116 | ) 117 | rescue What3Words::API::ResponseError => e 118 | expect(e.message).to include('QuotaExceeded') 119 | end 120 | end 121 | 122 | it 'converts coordinates to a 3 word address in French' do 123 | begin 124 | result = w3w.convert_to_3wa([29.567041, 106.587875], language: 'fr', format: 'json') 125 | expect(result).to include( 126 | words: 'courgette.rabotons.infrason', 127 | language: 'fr' 128 | ) 129 | rescue What3Words::API::ResponseError => e 130 | expect(e.message).to include('QuotaExceeded') 131 | end 132 | end 133 | end 134 | 135 | describe 'autosuggest' do 136 | it 'returns suggestions for a valid input' do 137 | result = w3w.autosuggest('filled.count.soap') 138 | expect(result[:suggestions]).not_to be_empty 139 | end 140 | 141 | it 'returns the default number of suggestions for a simple input' do 142 | result = w3w.autosuggest('disclose.strain.redefin', language: 'en') 143 | expect(result[:suggestions].count).to eq(3) 144 | end 145 | 146 | it 'returns suggestions in the specified language' do 147 | result = w3w.autosuggest('trop.caler.perdre', language: 'fr') 148 | result[:suggestions].each do |suggestion| 149 | expect(suggestion[:language]).to eq('fr') 150 | end 151 | end 152 | 153 | it 'returns suggestions for an input in Arabic' do 154 | result = w3w.autosuggest('مربية.الصباح.المده', language: 'ar') 155 | expect(result[:suggestions]).not_to be_empty 156 | end 157 | 158 | it 'returns a specified number of results' do 159 | result = w3w.autosuggest('disclose.strain.redefin', language: 'en', 'n-results': 10) 160 | expect(result[:suggestions].count).to be >= 10 161 | end 162 | 163 | it 'returns suggestions with focus parameter' do 164 | result = w3w.autosuggest('filled.count.soap', focus: [51.4243877, -0.34745]) 165 | expect(result[:suggestions]).not_to be_empty 166 | end 167 | 168 | it 'returns focused suggestions' do 169 | result = w3w.autosuggest('disclose.strain.redefin', language: 'en', 'n-focus-results': 3) 170 | expect(result[:suggestions].count).to be >= 3 171 | end 172 | 173 | it 'returns suggestions for generic-voice input type' do 174 | # @:param string input-type: For power users, used to specify voice input mode. Can be 175 | # text (default), vocon-hybrid, nmdp-asr or generic-voice. 176 | result = w3w.autosuggest 'fun with code', 'input-type': 'generic-voice', language: 'en' 177 | suggestions = result[:suggestions] 178 | output = ['fund.with.code', 'funds.with.code', 'fund.whiff.code'] 179 | suggestions.each_with_index do |item, index| 180 | # puts item[:words] 181 | expect(item[:words]).to eq(output[index]) 182 | end 183 | 184 | expect(result).not_to be_empty 185 | end 186 | 187 | it 'returns different suggestions with prefer-land parameter' do 188 | result_sea = w3w.autosuggest('///yourselves.frolicking.supernova', 'prefer-land': true, 'n-results': 1) 189 | result_land = w3w.autosuggest('///yourselves.frolicking.supernov', 'prefer-land': false,'n-results': 1) 190 | 191 | # puts "Sea suggestions: #{result_sea[:suggestions]}" 192 | # puts "Land suggestions: #{result_land[:suggestions]}" 193 | 194 | # Check if the suggestions arrays have different lengths or elements 195 | suggestions_different = (result_sea[:suggestions].length != result_land[:suggestions].length) || 196 | (result_sea[:suggestions] != result_land[:suggestions]) 197 | 198 | expect(suggestions_different).to be true 199 | end 200 | 201 | it 'returns suggestions within specified countries' do 202 | result = w3w.autosuggest('disclose.strain.redefin', 'clip-to-country': 'GB,BE') 203 | result[:suggestions].each do |suggestion| 204 | expect(['GB', 'BE']).to include(suggestion[:country]) 205 | end 206 | end 207 | 208 | it 'returns suggestions within a specified bounding box' do 209 | result = w3w.autosuggest('disclose.strain.redefin', 'clip-to-bounding-box': [51.521, -0.343, 52.6, 2.3324]) 210 | expect(result[:suggestions]).to include( 211 | country: 'GB', 212 | nearestPlace: 'Saxmundham, Suffolk', 213 | words: 'discloses.strain.reddish', 214 | rank: 1, 215 | language: 'en' 216 | ) 217 | end 218 | 219 | it 'raises an error with an invalid bounding box' do 220 | expect { w3w.autosuggest('disclose.strain.redefin', 'clip-to-bounding-box': [51.521, -0.343, 52.6]) } 221 | .to raise_error(described_class::ResponseError) 222 | end 223 | 224 | it 'raises an error with a second invalid bounding box' do 225 | expect { w3w.autosuggest('disclose.strain.redefin', 'clip-to-bounding-box': [51.521, -0.343, 55.521, -5.343]) } 226 | .to raise_error(described_class::ResponseError) 227 | end 228 | 229 | it 'returns suggestions within a specified circle' do 230 | result = w3w.autosuggest('disclose.strain.redefin', 'clip-to-circle': [51.521, -0.343, 142]) 231 | expect(result[:suggestions]).to include( 232 | country: 'GB', 233 | nearestPlace: 'Market Harborough, Leicestershire', 234 | words: 'discloses.strain.reduce', 235 | rank: 1, 236 | language: 'en' 237 | ) 238 | end 239 | 240 | it 'returns suggestions within a specified polygon' do 241 | result = w3w.autosuggest('disclose.strain.redefin', 'clip-to-polygon': [51.521, -0.343, 52.6, 2.3324, 54.234, 8.343, 51.521, -0.343]) 242 | expect(result[:suggestions]).to include( 243 | country: 'GB', 244 | nearestPlace: 'Saxmundham, Suffolk', 245 | words: 'discloses.strain.reddish', 246 | rank: 1, 247 | language: 'en' 248 | ) 249 | end 250 | end 251 | 252 | describe 'grid_section' do 253 | it 'returns a grid section for a valid bounding box' do 254 | begin 255 | result = w3w.grid_section('52.208867,0.117540,52.207988,0.116126') 256 | expect(result).not_to be_empty 257 | rescue What3Words::API::ResponseError => e 258 | expect(e.message).to include('QuotaExceeded') 259 | end 260 | end 261 | 262 | it 'raises an error for an invalid bounding box' do 263 | begin 264 | expect { w3w.grid_section('50.0,178,50.01,180.0005') } 265 | .to raise_error(What3Words::API::ResponseError) 266 | rescue What3Words::API::ResponseError => e 267 | expect(e.message).to include('QuotaExceeded') 268 | end 269 | end 270 | end 271 | 272 | describe 'available_languages' do 273 | it 'retrieves all available languages' do 274 | result = w3w.available_languages 275 | expect(result[:languages].count).to be >= 51 276 | end 277 | 278 | it 'does not return an empty list of languages' do 279 | result = w3w.available_languages 280 | expect(result[:languages]).not_to be_empty 281 | end 282 | end 283 | 284 | describe 'isPossible3wa' do 285 | it 'returns true for a valid 3 word address' do 286 | expect(w3w.isPossible3wa('filled.count.soap')).to be true 287 | end 288 | 289 | it 'returns false for an invalid address with spaces' do 290 | expect(w3w.isPossible3wa('not a 3wa')).to be false 291 | end 292 | 293 | it 'returns false for an invalid address with mixed formats' do 294 | expect(w3w.isPossible3wa('not.3wa address')).to be false 295 | end 296 | end 297 | 298 | describe 'findPossible3wa' do 299 | it 'finds a single 3 word address in text' do 300 | text = "Please leave by my porch at filled.count.soap" 301 | expect(w3w.findPossible3wa(text)).to eq(['filled.count.soap']) 302 | end 303 | 304 | it 'finds multiple 3 word addresses in text' do 305 | text = "Please leave by my porch at filled.count.soap or deed.tulip.judge" 306 | expect(w3w.findPossible3wa(text)).to eq(['filled.count.soap', 'deed.tulip.judge']) 307 | end 308 | 309 | it 'returns an empty array when no 3 word address is found' do 310 | text = "Please leave by my porch at" 311 | expect(w3w.findPossible3wa(text)).to eq([]) 312 | end 313 | end 314 | 315 | describe 'didYouMean' do 316 | it 'returns true for valid three word address with hyphens' do 317 | expect(w3w.didYouMean('filled-count-soap')).to be true 318 | end 319 | 320 | it 'returns true for valid three word address with spaces' do 321 | expect(w3w.didYouMean('filled count soap')).to be true 322 | end 323 | 324 | it 'returns false for invalid address with special characters' do 325 | expect(w3w.didYouMean('invalid#address!example')).to be false 326 | end 327 | 328 | it 'returns false for random text not in w3w format' do 329 | expect(w3w.didYouMean('this is not a w3w address')).to be false 330 | end 331 | end 332 | 333 | describe 'isValid3wa' do 334 | it 'returns true for a valid 3 word address' do 335 | expect(w3w.isValid3wa('filled.count.soap')).to be true 336 | end 337 | 338 | it 'returns false for an invalid 3 word address' do 339 | expect(w3w.isValid3wa('invalid.address.here')).to be false 340 | end 341 | 342 | it 'returns false for a random string' do 343 | expect(w3w.isValid3wa('this is not a w3w address')).to be false 344 | end 345 | end 346 | 347 | describe 'technical' do 348 | it 'deep_symbolize_keys helper works correctly' do 349 | expect(w3w.send(:deep_symbolize_keys, 'foo' => { 'bar' => true })) 350 | .to eq(foo: { bar: true }) 351 | end 352 | end 353 | end 354 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | 5 | Bundler.setup 6 | 7 | require 'webmock/rspec' 8 | 9 | require 'what3words' 10 | -------------------------------------------------------------------------------- /what3words.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'what3words/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'what3words' 9 | spec.version = What3Words::VERSION 10 | spec.authors = ['what3words'] 11 | spec.email = ['development@what3words.com'] 12 | spec.description = 'A Ruby wrapper for the what3words API' 13 | spec.summary = 'Ruby wrapper for the what3words API' 14 | spec.homepage = 'https://github.com/what3words/w3w-ruby-wrapper' 15 | spec.license = 'MIT' 16 | 17 | spec.files = Dir["lib/**/*.rb", "README.md"] 18 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 19 | spec.test_files = spec.files.grep(%r{^spec/}) 20 | spec.require_paths = ['lib'] 21 | spec.platform = Gem::Platform::RUBY 22 | spec.required_ruby_version = '>= 2.6', '< 4.0' 23 | 24 | 25 | spec.metadata["documentation_uri"] = "https://www.rubydoc.info/gems/what3words" 26 | spec.metadata["source_code_uri"] = "https://github.com/what3words/w3w-ruby-wrapper" 27 | 28 | spec.add_dependency('rest-client', '>= 1.8', '< 3.0') 29 | 30 | spec.add_development_dependency 'bundler', '~> 2.1' 31 | spec.add_development_dependency 'rake', '~> 12.3' 32 | spec.add_development_dependency 'rspec', '~> 3.4' 33 | spec.add_development_dependency 'rubocop', '~> 0.49.0' 34 | spec.add_development_dependency 'webmock', '~> 3.0' 35 | end 36 | --------------------------------------------------------------------------------