piplapis-ruby 4.0.0

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/LICENSE ADDED
@@ -0,0 +1,11 @@
1
+ Licensed under the Apache License, Version 2.0 (the "License");
2
+ you may not use this file except in compliance with the License.
3
+ You may obtain a copy of the License at
4
+
5
+ http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software
8
+ distributed under the License is distributed on an "AS IS" BASIS,
9
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ See the License for the specific language governing permissions and
11
+ limitations under the License.
@@ -0,0 +1,38 @@
1
+ piplapis Ruby Library
2
+ ===========================
3
+
4
+ This is a Ruby client library for easily integrating Pipl's APIs into your application.
5
+
6
+ * Full details about Pipl's APIs - [http://dev.pipl.com](http://dev.pipl.com)
7
+ * This library is available in other languages - [http://dev.pipl.com/docs/libraries](http://dev.pipl.com/docs/libraries)
8
+
9
+ Library Requirements
10
+ --------------------
11
+
12
+ * Ruby 1.9 and above.
13
+
14
+ Library Notes
15
+ -------------
16
+
17
+ * According to the [documentation](http://dev.pipl.com/docs/read/search_api/data#fields-objects) some field objects should have a `.display` attribute,
18
+ since this is reserved in Ruby this attribute was renamed to `.show`.
19
+
20
+ Getting Started & Code Snippets
21
+ -------------------------------
22
+
23
+ **Pipl's Search API**
24
+ * Getting started tutorial - [http://dev.pipl.com/docs/search_api/getstarted](http://dev.pipl.com/docs/search_api/getstarted)
25
+ * Code snippets - [http://dev.pipl.com/docs/search_api/code](http://dev.pipl.com/docs/search_api/code)
26
+
27
+ **Pipl's Name API**
28
+ * Getting started tutorial - [http://dev.pipl.com/docs/name_api/getstarted](http://dev.pipl.com/docs/name_api/getstarted)
29
+ * Code snippets - [http://dev.pipl.com/docs/name_api/code](http://dev.pipl.com/docs/name_api/code)
30
+
31
+ **Pipl's Thumbnail API**
32
+ * Getting started tutorial - [http://dev.pipl.com/docs/thumbnail_api/getstarted](http://dev.pipl.com/docs/thumbnail_api/getstarted)
33
+ * Code snippets - [http://dev.pipl.com/docs/thumbnail_api/code](http://dev.pipl.com/docs/thumbnail_api/code)
34
+
35
+ Credits
36
+ -------
37
+
38
+ Thanks to [srochy](https://github.com/srochy) for writing this library!
@@ -0,0 +1,35 @@
1
+ require_relative 'pipl/client'
2
+ require_relative 'pipl/default'
3
+
4
+ module Pipl
5
+
6
+ class << self
7
+ include Pipl::Configurable
8
+
9
+ attr_accessor :logger
10
+
11
+ def client
12
+ @client = Client.new(options) unless defined?(@client) && @client.same_options?(options)
13
+ @client
14
+ end
15
+
16
+
17
+ def respond_to_missing?(method_name, include_private=false)
18
+ ; client.respond_to?(method_name, include_private);
19
+ end if RUBY_VERSION >= '1.9'
20
+
21
+ def respond_to?(method_name, include_private=false)
22
+ ; client.respond_to?(method_name, include_private) || super;
23
+ end if RUBY_VERSION < '1.9'
24
+
25
+ private
26
+
27
+ def method_missing(method_name, *args, &block)
28
+ return super unless client.respond_to?(method_name)
29
+ client.send(method_name, *args, &block)
30
+ end
31
+
32
+ end
33
+ end
34
+
35
+ Pipl.setup
@@ -0,0 +1,165 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+
4
+ require_relative 'configurable'
5
+ require_relative 'containers'
6
+ require_relative 'errors'
7
+ require_relative 'fields'
8
+ require_relative 'response'
9
+
10
+
11
+ module Pipl
12
+
13
+ class Client
14
+
15
+ include Pipl::Configurable
16
+
17
+ def initialize(options = {})
18
+ Pipl::Configurable.keys.each do |key|
19
+ instance_variable_set(:"@#{key}", options[key] || Pipl.instance_variable_get(:"@#{key}"))
20
+ end
21
+
22
+ end
23
+
24
+ def same_options?(opts)
25
+ opts.hash == options.hash
26
+ end
27
+
28
+ def search(params={})
29
+ opts = options.merge params
30
+ create_search_person(opts)
31
+ validate_search_params(opts)
32
+ http, req = create_http_request(opts)
33
+ if opts.key? :callback
34
+ do_send_async http, req, opts[:callback]
35
+ elsif opts[:async] and block_given?
36
+ do_send_async http, req, Proc.new
37
+ else
38
+ do_send http, req
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def create_search_person(opts)
45
+ return if opts.key? :search_pointer
46
+
47
+ person = opts[:person] || Pipl::Person.new
48
+ person.add_field Pipl::Name.new(raw: opts[:raw_name]) if opts[:raw_name]
49
+ person.add_field Pipl::Email.new(address: opts[:email]) if opts[:email]
50
+ person.add_field Pipl::Username.new(content: opts[:username]) if opts[:username]
51
+ person.add_field Pipl::Address.new(raw: opts[:raw_address]) if opts[:raw_address]
52
+
53
+ if opts[:first_name] || opts[:middle_name] || opts[:last_name]
54
+ person.add_field Pipl::Name.new(first: opts[:first_name], middle: opts[:middle_name], last: opts[:last_name])
55
+ end
56
+
57
+ if opts[:country] || opts[:state] || opts[:city]
58
+ person.add_field Pipl::Address.new(country: opts[:country], state: opts[:state], city: opts[:city])
59
+ end
60
+
61
+ if opts[:phone]
62
+ if opts[:phone].is_a? String
63
+ person.add_field Pipl::Phone.new(raw: opts[:phone])
64
+ else
65
+ person.add_field Pipl::Phone.new(number: opts[:phone])
66
+ end
67
+ end
68
+
69
+ if opts[:from_age] || opts[:to_age]
70
+ person.add_field Pipl::DOB.from_age_range((opts[:from_age] || 0).to_i, (opts[:to_age].to_i || 1000).to_i)
71
+ end
72
+
73
+ opts[:person] = person
74
+ end
75
+
76
+ def validate_search_params(opts)
77
+ unless opts[:api_key] and not opts[:api_key].empty?
78
+ raise ArgumentError.new('API key is missing')
79
+ end
80
+
81
+ if opts[:search_pointer] and opts[:search_pointer].empty?
82
+ raise ArgumentError.new('Given search pointer is empty')
83
+ end
84
+
85
+ unless opts.key? :search_pointer
86
+ unless opts[:person] and opts[:person].is_searchable?
87
+ raise ArgumentError.new('At least one valid name/username/phone/email is required for search')
88
+ end
89
+ end
90
+
91
+ if opts[:strict_validation]
92
+ if opts[:minimum_probability] and not (0..1).include? opts[:minimum_probability]
93
+ raise ArgumentError.new('minimum_probability must be a float in 0..1')
94
+ end
95
+
96
+ if opts[:minimum_match] and not (0..1).include? opts[:minimum_match]
97
+ raise ArgumentError.new('minimum_match must be a float in 0..1')
98
+ end
99
+
100
+ unless [Pipl::Configurable::SHOW_SOURCES_ALL,
101
+ Pipl::Configurable::SHOW_SOURCES_MATCHING,
102
+ Pipl::Configurable::SHOW_SOURCES_NONE, nil].include? opts[:show_sources]
103
+ raise ArgumentError.new('show_sources must be one of all, matching or false')
104
+ end
105
+
106
+ unless opts.key? :search_pointer
107
+ unsearchable = opts[:person].unsearchable_fields
108
+ if unsearchable and not unsearchable.empty?
109
+ raise ArgumentError.new("Some fields are unsearchable: #{unsearchable}")
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ def create_http_request(opts)
116
+ uri = URI(opts[:api_endpoint])
117
+ keys = %w(minimum_probability minimum_match hide_sponsored live_feeds show_sources)
118
+ query_params = ["key=#{opts[:api_key]}"] + keys.map { |k| "#{k}=#{opts[k.to_sym]}" unless opts[k.to_sym].nil? }
119
+ uri.query = query_params.compact.join('&')
120
+
121
+ req = Net::HTTP::Post.new(uri.request_uri)
122
+ req['User-Agent'] = opts[:user_agent]
123
+ if opts.key? :search_pointer
124
+ req.set_form_data search_pointer: opts[:search_pointer]
125
+ else
126
+ h = opts[:person].to_hash
127
+ req.set_form_data person: h.reject { |_, value| value.nil? }.to_json
128
+ end
129
+
130
+ http = Net::HTTP.new(uri.host, uri.port)
131
+ http.use_ssl = uri.scheme == 'https'
132
+ http.open_timeout = opts[:http_timeout]
133
+
134
+ [http, req]
135
+ end
136
+
137
+ def do_send(http, req)
138
+ response = http.request(req)
139
+ if response.is_a? Net::HTTPSuccess
140
+ SearchResponse.from_json(response.body)
141
+ else
142
+ begin
143
+ err = Pipl::Client::APIError.from_json(response.body)
144
+ rescue
145
+ err = Pipl::Client::APIError.new response.message, response.code
146
+ end
147
+ raise err
148
+ end
149
+ end
150
+
151
+ def do_send_async(http, req, callback)
152
+ Thread.new do
153
+ begin
154
+ response = do_send http, req
155
+ callback.call response: response
156
+ rescue Pipl::Client::APIError => err
157
+ callback.call error: err
158
+ rescue Exception => msg
159
+ callback.call error: msg
160
+ end
161
+ end
162
+ end
163
+
164
+ end
165
+ end
@@ -0,0 +1,55 @@
1
+ module Pipl
2
+
3
+ module Configurable
4
+
5
+ SHOW_SOURCES_ALL = 'all'
6
+ SHOW_SOURCES_MATCHING = 'matching'
7
+ SHOW_SOURCES_NONE = 'false'
8
+
9
+ attr_accessor :api_key, :minimum_probability, :minimum_match, :hide_sponsored, :live_feeds, :show_sources
10
+ attr_accessor :strict_validation, :user_agent
11
+ attr_writer :api_endpoint
12
+
13
+ class << self
14
+
15
+ def keys
16
+ @keys ||= [
17
+ :api_key,
18
+ :minimum_probability,
19
+ :minimum_match,
20
+ :hide_sponsored,
21
+ :live_feeds,
22
+ :show_sources,
23
+ :strict_validation,
24
+ :api_endpoint,
25
+ :user_agent
26
+ ]
27
+ end
28
+
29
+ end
30
+
31
+ def configure
32
+ yield self
33
+ end
34
+
35
+ def reset!
36
+ Pipl::Configurable.keys.each do |key|
37
+ instance_variable_set(:"@#{key}", Pipl::Default.options[key])
38
+ end
39
+ self
40
+ end
41
+
42
+ alias setup reset!
43
+
44
+ def api_endpoint
45
+ File.join(@api_endpoint, '')
46
+ end
47
+
48
+ private
49
+
50
+ def options
51
+ Hash[Pipl::Configurable.keys.map { |key| [key, instance_variable_get(:"@#{key}")] }]
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,74 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module Pipl
4
+
5
+ DATE_FORMAT = '%Y-%m-%d'
6
+
7
+ STATES = {
8
+ US: {WA: 'Washington', VA: 'Virginia', DE: 'Delaware', DC: 'District Of Columbia', WI: 'Wisconsin',
9
+ WV: 'West Virginia', HI: 'Hawaii', FL: 'Florida', YT: 'Yukon', WY: 'Wyoming', PR: 'Puerto Rico',
10
+ NJ: 'New Jersey', NM: 'New Mexico', TX: 'Texas', LA: 'Louisiana', NC: 'North Carolina', ND: 'North Dakota',
11
+ NE: 'Nebraska', FM: 'Federated States Of Micronesia', TN: 'Tennessee', NY: 'New York', PA: 'Pennsylvania',
12
+ CT: 'Connecticut', RI: 'Rhode Island', NV: 'Nevada', NH: 'New Hampshire', GU: 'Guam', CO: 'Colorado',
13
+ VI: 'Virgin Islands', AK: 'Alaska', AL: 'Alabama', AS: 'American Samoa', AR: 'Arkansas', VT: 'Vermont',
14
+ IL: 'Illinois', GA: 'Georgia', IN: 'Indiana', IA: 'Iowa', MA: 'Massachusetts', AZ: 'Arizona',
15
+ CA: 'California', ID: 'Idaho', PW: 'Pala', ME: 'Maine', MD: 'Maryland', OK: 'Oklahoma', OH: 'Ohio',
16
+ UT: 'Utah', MO: 'Missouri', MN: 'Minnesota', MI: 'Michigan', MH: 'Marshall Islands', KS: 'Kansas',
17
+ MT: 'Montana', MP: 'Northern Mariana Islands', MS: 'Mississippi', SC: 'South Carolina', KY: 'Kentucky',
18
+ OR: 'Oregon', SD: 'South Dakota'},
19
+ CA: {AB: 'Alberta', BC: 'British Columbia', MB: 'Manitoba', NB: 'New Brunswick', NT: 'Northwest Territories',
20
+ NS: 'Nova Scotia', NU: 'Nunavut', ON: 'Ontario', PE: 'Prince Edward Island', QC: 'Quebec',
21
+ SK: 'Saskatchewan', YU: 'Yukon', NL: 'Newfoundland and Labrador'},
22
+ AU: {WA: 'State of Western Australia', SA: 'State of South Australia', NT: 'Northern Territory',
23
+ VIC: 'State of Victoria', TAS: 'State of Tasmania', QLD: 'State of Queensland',
24
+ NSW: 'State of New South Wales', ACT: 'Australian Capital Territory'},
25
+ GB: {WLS: 'Wales', SCT: 'Scotland', NIR: 'Northern Ireland', ENG: 'England'}
26
+ }
27
+
28
+ COUNTRIES = {BD: 'Bangladesh', WF: 'Wallis And Futuna Islands', BF: 'Burkina Faso', PY: 'Paraguay',
29
+ BA: 'Bosnia And Herzegovina', BB: 'Barbados', BE: 'Belgium', BM: 'Bermuda', BN: 'Brunei Darussalam',
30
+ BO: 'Bolivia', BH: 'Bahrain', BI: 'Burundi', BJ: 'Benin', BT: 'Bhutan', JM: 'Jamaica',
31
+ BV: 'Bouvet Island', BW: 'Botswana', WS: 'Samoa', BR: 'Brazil', BS: 'Bahamas', JE: 'Jersey',
32
+ BY: 'Belarus', BZ: 'Belize', RU: 'Russian Federation', RW: 'Rwanda', LT: 'Lithuania', RE: 'Reunion',
33
+ TM: 'Turkmenistan', TJ: 'Tajikistan', RO: 'Romania', LS: 'Lesotho', GW: 'Guinea-bissa', GU: 'Guam',
34
+ GT: 'Guatemala', GS: 'South Georgia And South Sandwich Islands', GR: 'Greece', GQ: 'Equatorial Guinea',
35
+ GP: 'Guadeloupe', JP: 'Japan', GY: 'Guyana', GG: 'Guernsey', GF: 'French Guiana', GE: 'Georgia',
36
+ GD: 'Grenada', GB: 'Great Britain', GA: 'Gabon', GN: 'Guinea', GM: 'Gambia', GL: 'Greenland',
37
+ GI: 'Gibraltar', GH: 'Ghana', OM: 'Oman', TN: 'Tunisia', JO: 'Jordan', HR: 'Croatia', HT: 'Haiti',
38
+ SV: 'El Salvador', HK: 'Hong Kong', HN: 'Honduras', HM: 'Heard And Mcdonald Islands', AD: 'Andorra',
39
+ PR: 'Puerto Rico', PS: 'Palestine', PW: 'Pala', PT: 'Portugal', SJ: 'Svalbard And Jan Mayen Islands',
40
+ VG: 'Virgin Islands, British', AI: 'Anguilla', KP: 'North Korea', PF: 'French Polynesia',
41
+ PG: 'Papua New Guinea', PE: 'Per', PK: 'Pakistan', PH: 'Philippines', PN: 'Pitcairn', PL: 'Poland',
42
+ PM: 'Saint Pierre And Miquelon', ZM: 'Zambia', EH: 'Western Sahara', EE: 'Estonia', EG: 'Egypt',
43
+ ZA: 'South Africa', EC: 'Ecuador', IT: 'Italy', AO: 'Angola', KZ: 'Kazakhstan', ET: 'Ethiopia',
44
+ ZW: 'Zimbabwe', SA: 'Saudi Arabia', ES: 'Spain', ER: 'Eritrea', ME: 'Montenegro', MD: 'Moldova',
45
+ MG: 'Madagascar', MA: 'Morocco', MC: 'Monaco', UZ: 'Uzbekistan', MM: 'Myanmar', ML: 'Mali', MO: 'Maca',
46
+ MN: 'Mongolia', MH: 'Marshall Islands', US: 'United States', UM: 'United States Minor Outlying Islands',
47
+ MT: 'Malta', MW: 'Malawi', MV: 'Maldives', MQ: 'Martinique', MP: 'Northern Mariana Islands',
48
+ MS: 'Montserrat', NA: 'Namibia', IM: 'Isle Of Man', UG: 'Uganda', MY: 'Malaysia', MX: 'Mexico',
49
+ IL: 'Israel', BG: 'Bulgaria', FR: 'France', AW: 'Aruba', AX: 'Åland', FI: 'Finland',
50
+ FJ: 'Fiji', FK: 'Falkland Islands', FM: 'Micronesia', FO: 'Faroe Islands', NI: 'Nicaragua',
51
+ NL: 'Netherlands', NO: 'Norway', SO: 'Somalia', NC: 'New Caledonia', NE: 'Niger', NF: 'Norfolk Island',
52
+ NG: 'Nigeria', NZ: 'New Zealand', NP: 'Nepal', NR: 'Naur', NU: 'Niue', MR: 'Mauritania',
53
+ CK: 'Cook Islands', CI: "Côte D'ivoire", CH: 'Switzerland', CO: 'Colombia', CN: 'China',
54
+ CM: 'Cameroon', CL: 'Chile', CC: 'Cocos (keeling) Islands', CA: 'Canada', CG: 'Congo (brazzaville)',
55
+ CF: 'Central African Republic', CD: 'Congo (kinshasa)', CZ: 'Czech Republic', CY: 'Cyprus',
56
+ CX: 'Christmas Island', CS: 'Serbia', CR: 'Costa Rica', HU: 'Hungary', CV: 'Cape Verde', CU: 'Cuba',
57
+ SZ: 'Swaziland', SY: 'Syria', KG: 'Kyrgyzstan', KE: 'Kenya', SR: 'Suriname', KI: 'Kiribati',
58
+ KH: 'Cambodia', KN: 'Saint Kitts And Nevis', KM: 'Comoros', ST: 'Sao Tome And Principe', SK: 'Slovakia',
59
+ KR: 'South Korea', SI: 'Slovenia', SH: 'Saint Helena', KW: 'Kuwait', SN: 'Senegal', SM: 'San Marino',
60
+ SL: 'Sierra Leone', SC: 'Seychelles', SB: 'Solomon Islands', KY: 'Cayman Islands', SG: 'Singapore',
61
+ SE: 'Sweden', SD: 'Sudan', DO: 'Dominican Republic', DM: 'Dominica', DJ: 'Djibouti', DK: 'Denmark',
62
+ DE: 'Germany', YE: 'Yemen', AT: 'Austria', DZ: 'Algeria', MK: 'Macedonia', UY: 'Uruguay', YT: 'Mayotte',
63
+ MU: 'Mauritius', TZ: 'Tanzania', LC: 'Saint Lucia', LA: 'Laos', TV: 'Tuval', TW: 'Taiwan',
64
+ TT: 'Trinidad And Tobago', TR: 'Turkey', LK: 'Sri Lanka', LI: 'Liechtenstein', LV: 'Latvia',
65
+ TO: 'Tonga', TL: 'Timor-leste', LU: 'Luxembourg', LR: 'Liberia', TK: 'Tokela', TH: 'Thailand',
66
+ TF: 'French Southern Lands', TG: 'Togo', TD: 'Chad', TC: 'Turks And Caicos Islands', LY: 'Libya',
67
+ VA: 'Vatican City', AC: 'Ascension Island', VC: 'Saint Vincent And The Grenadines',
68
+ AE: 'United Arab Emirates', VE: 'Venezuela', AG: 'Antigua And Barbuda', AF: 'Afghanistan', IQ: 'Iraq',
69
+ VI: 'Virgin Islands, U.s.', IS: 'Iceland', IR: 'Iran', AM: 'Armenia', AL: 'Albania', VN: 'Vietnam',
70
+ AN: 'Netherlands Antilles', AQ: 'Antarctica', AS: 'American Samoa', AR: 'Argentina', AU: 'Australia',
71
+ VU: 'Vanuat', IO: 'British Indian Ocean Territory', IN: 'India', LB: 'Lebanon', AZ: 'Azerbaijan',
72
+ IE: 'Ireland', ID: 'Indonesia', PA: 'Panama', UA: 'Ukraine', QA: 'Qatar', MZ: 'Mozambique'}
73
+
74
+ end
@@ -0,0 +1,289 @@
1
+ require_relative 'fields'
2
+ require_relative 'utils'
3
+
4
+
5
+ module Pipl
6
+
7
+ class FieldsContainer
8
+
9
+ CLASS_CONTAINER = {
10
+ Name: 'names',
11
+ Address: 'addresses',
12
+ Phone: 'phones',
13
+ Email: 'emails',
14
+ Job: 'jobs',
15
+ Education: 'educations',
16
+ Image: 'images',
17
+ Username: 'usernames',
18
+ UserID: 'user_ids',
19
+ Url: 'urls',
20
+ Ethnicity: 'ethnicities',
21
+ Language: 'languages',
22
+ OriginCountry: 'origin_countries',
23
+ Relationship: 'relationships',
24
+ Tag: 'tags',
25
+ }
26
+
27
+ attr_reader :names, :addresses, :phones, :emails, :jobs, :educations, :images, :usernames, :user_ids, :urls
28
+ attr_reader :relationships, :tags, :ethnicities, :languages, :origin_countries, :dob, :gender
29
+
30
+ def initialize(params={})
31
+ @names = []
32
+ @addresses = []
33
+ @phones = []
34
+ @emails = []
35
+ @jobs = []
36
+ @educations = []
37
+ @images = []
38
+ @usernames = []
39
+ @user_ids = []
40
+ @urls = []
41
+ @ethnicities = []
42
+ @languages = []
43
+ @origin_countries = []
44
+ @relationships = []
45
+ @tags = []
46
+ @dob = nil
47
+ @gender = nil
48
+
49
+ add_fields params[:fields] if params.key? :fields
50
+ end
51
+
52
+ def self.from_hash(h)
53
+ raise AbstractMethodInvoked.new
54
+ end
55
+
56
+ def self.fields_from_hash(h)
57
+ fields = self::CLASS_CONTAINER.map do |cls_name, container|
58
+ cls = Pipl.const_get cls_name
59
+ h[container.to_sym].map { |x| cls.from_hash(x) } if h.key? container.to_sym
60
+ end
61
+ .flatten.compact
62
+ fields << DOB.from_hash(h[:dob]) if h.key? :dob
63
+ fields << Gender.from_hash(h[:gender]) if h.key? :gender
64
+ fields
65
+ end
66
+
67
+ def fields_to_hash
68
+ h = {}
69
+ h[:dob] = @dob.to_hash if @dob
70
+ h[:gender] = @gender.to_hash if @gender
71
+ self.class::CLASS_CONTAINER.values.each do |container|
72
+ fields = instance_variable_get("@#{container}")
73
+ h[container.to_sym] = fields.map { |field| field.to_hash }.compact unless fields.empty?
74
+ end
75
+ h.reject { |_, value| value.nil? or (value.kind_of?(Array) and value.empty?) }
76
+ end
77
+
78
+ def add_fields(fields)
79
+ fields.each { |f| add_field f }
80
+ end
81
+
82
+ def add_field(field)
83
+ cls_sym = field.class.name.split('::').last.to_sym
84
+ container = self.class::CLASS_CONTAINER[cls_sym]
85
+ if container
86
+ instance_variable_get("@#{container}") << field
87
+ elsif cls_sym == :DOB
88
+ @dob = field
89
+ elsif cls_sym == :Gender
90
+ @gender = field
91
+ else
92
+ raise ArgumentError.new("Object of type #{field.class} is an invalid field")
93
+ end
94
+ end
95
+
96
+ def all_fields
97
+ fields = self.class::CLASS_CONTAINER.values.map { |container| instance_variable_get("@#{container}") }
98
+ .flatten.compact
99
+ fields << @dob if @dob
100
+ fields << @gender if @gender
101
+ fields
102
+ end
103
+
104
+ def job
105
+ @jobs.first unless @jobs.empty?
106
+ end
107
+
108
+ def address
109
+ @addresses.first unless @addresses.empty?
110
+ end
111
+
112
+ def education
113
+ @educations.first unless @educations.empty?
114
+ end
115
+
116
+ def language
117
+ @languages.first unless @languages.empty?
118
+ end
119
+
120
+ def ethnicity
121
+ @ethnicities.first unless @ethnicities.empty?
122
+ end
123
+
124
+ def origin_country
125
+ @origin_countries.first unless @origin_countries.empty?
126
+ end
127
+
128
+ def phone
129
+ @phones.first unless @phones.empty?
130
+ end
131
+
132
+ def email
133
+ @emails.first unless @emails.empty?
134
+ end
135
+
136
+ def name
137
+ @names.first unless @names.empty?
138
+ end
139
+
140
+ def image
141
+ @images.first unless @images.empty?
142
+ end
143
+
144
+ def url
145
+ @urls.first unless @urls.empty?
146
+ end
147
+
148
+ def username
149
+ @usernames.first unless @usernames.empty?
150
+ end
151
+
152
+ def user_id
153
+ @user_ids.first unless @user_ids.empty?
154
+ end
155
+
156
+ def relationship
157
+ @relationships.first unless @relationships.empty?
158
+ end
159
+
160
+ end
161
+
162
+
163
+ class Relationship < FieldsContainer
164
+
165
+ CLASS_CONTAINER = FieldsContainer::CLASS_CONTAINER.clone
166
+ CLASS_CONTAINER.delete :Relationship
167
+
168
+ # @!attribute valid_since
169
+ # @see Field
170
+ # @!attribute inferred
171
+ # @see Field
172
+ # @!attribute type
173
+ # @return [String] Type of association of this relationship to a person.
174
+ # Possible values are:
175
+ # friend
176
+ # family
177
+ # work
178
+ # other
179
+ # @!attribute subtype
180
+ # @return [String] Subtype of association of this relationship to a person. Free text.
181
+
182
+ attr_accessor :valid_since, :inferred, :type, :subtype
183
+
184
+ def initialize(params={})
185
+ super params
186
+ @valid_since = params[:valid_since]
187
+ @inferred = params[:inferred]
188
+ @type = params[:type]
189
+ @subtype = params[:subtype]
190
+ end
191
+
192
+ def self.from_hash(h)
193
+ params = Pipl::Field.base_params_from_hash h
194
+ params[:subtype] = h[:@subtype]
195
+ params[:fields] = self.fields_from_hash(h)
196
+ self.new(params)
197
+ end
198
+
199
+ def to_hash
200
+ fields_to_hash
201
+ end
202
+
203
+ def to_s
204
+ @names.first.to_s unless @names.empty?
205
+ end
206
+
207
+ end
208
+
209
+
210
+ class Source < FieldsContainer
211
+
212
+ attr_reader :match, :name, :category, :origin_url, :sponsored, :domain, :source_id, :person_id, :premium, :valid_since
213
+
214
+ def initialize(params={})
215
+ super params
216
+ @name = params[:name]
217
+ @category = params[:category]
218
+ @origin_url = params[:origin_url]
219
+ @domain = params[:domain]
220
+ @source_id = params[:source_id]
221
+ @person_id = params[:person_id]
222
+ @sponsored = params[:sponsored]
223
+ @premium = params[:premium]
224
+ @match = params[:match]
225
+ @valid_since = params[:valid_since]
226
+ end
227
+
228
+ def self.from_hash(h)
229
+ params = {
230
+ name: h[:@name],
231
+ category: h[:@category],
232
+ origin_url: h[:@origin_url],
233
+ domain: h[:@domain],
234
+ source_id: h[:@source_id],
235
+ person_id: h[:@person_id],
236
+ match: h[:@match],
237
+ sponsored: h[:@sponsored],
238
+ premium: h[:@premium],
239
+ }
240
+ params[:valid_since] = Pipl::Utils.str_to_date(h[:@valid_since]) if h.key? :@valid_since
241
+ params[:fields] = self.fields_from_hash(h)
242
+ self.new(params)
243
+ end
244
+
245
+ end
246
+
247
+ class Person < FieldsContainer
248
+
249
+ attr_reader :id, :match, :search_pointer
250
+
251
+ def initialize(params={})
252
+ super params
253
+ @id = params[:id]
254
+ @match = params[:match]
255
+ @search_pointer = params[:search_pointer]
256
+ end
257
+
258
+ def self.from_hash(h)
259
+ params = {
260
+ id: h[:@id],
261
+ match: h[:@match],
262
+ search_pointer: h[:@search_pointer],
263
+ }
264
+ params[:fields] = fields_from_hash(h)
265
+ self.new(params)
266
+ end
267
+
268
+ def to_hash
269
+ h = {}
270
+ h[:search_pointer] = @search_pointer if @search_pointer and not @search_pointer.empty?
271
+ h.update(fields_to_hash)
272
+ h
273
+ end
274
+
275
+ def is_searchable?
276
+ not @search_pointer.nil? or
277
+ @names.any? { |f| f.is_searchable? } or
278
+ @emails.any? { |f| f.is_searchable? } or
279
+ @phones.any? { |f| f.is_searchable? } or
280
+ @usernames.any? { |f| f.is_searchable? }
281
+ end
282
+
283
+ def unsearchable_fields
284
+ all_fields.reject { |f| f.is_searchable? }
285
+ end
286
+
287
+ end
288
+
289
+ end