typeform_data 0.0.1 → 0.0.2
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.
- checksums.yaml +4 -4
- data/README.md +7 -0
- data/bin/console +3 -2
- data/lib/typeform_data.rb +24 -1
- data/lib/typeform_data/client.rb +17 -10
- data/lib/typeform_data/config.rb +32 -0
- data/lib/typeform_data/errors.rb +1 -1
- data/lib/typeform_data/requestor.rb +16 -18
- data/lib/typeform_data/typeform.rb +58 -61
- data/lib/typeform_data/typeform/answer.rb +36 -0
- data/lib/typeform_data/typeform/by_id.rb +13 -0
- data/lib/typeform_data/typeform/field.rb +30 -0
- data/lib/typeform_data/typeform/question.rb +12 -15
- data/lib/typeform_data/typeform/response.rb +36 -18
- data/lib/typeform_data/typeform/stats.rb +2 -17
- data/lib/typeform_data/value_class.rb +73 -0
- data/lib/typeform_data/version.rb +1 -1
- data/typeform_data.gemspec +6 -5
- metadata +31 -14
- data/lib/typeform_data/requestor/config.rb +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 876713987fcf2c378c84995bf1a5c3b0d460755a
|
4
|
+
data.tar.gz: eaf7a7355ffda4252d318934b02aa806a215544d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3d0b3c91030dd7e26f4f9eb89ce2adf4ba02b7b6e09f885fa22ea1ad12979951cfb88e968d1272cfa9d093cfee8223151b32b8cbf9b469c1251f8435f3295afd
|
7
|
+
data.tar.gz: e1c74927ec69c7f570d25e5bf8e363faf4df9fde039ca91fee591d7bdebcd3d74ae33f0f6fb62336ee8245c0041e8c0300b348ce99b780392e7c5a615f4cf1ea
|
data/README.md
CHANGED
@@ -6,10 +6,17 @@ This is alpha software and doesn't currently cover all the use cases you'd proba
|
|
6
6
|
|
7
7
|
Unless you're eager to dive into the code, I'd suggest waiting until next week to check out this gem.
|
8
8
|
|
9
|
+
Ruby 2.3
|
10
|
+
|
9
11
|
TODO:
|
10
12
|
- Add more detail, and example method calls.
|
11
13
|
- Add an explanation: why another gem? What makes this gem different?
|
12
14
|
|
15
|
+
Goals:
|
16
|
+
- Typeform data (and invidual response data) must work with `Marshal#load` and `Marshal#dump`.
|
17
|
+
|
18
|
+
We have to do a lot of reverse-engineering to get an API that feels reasonably intuitive.
|
19
|
+
|
13
20
|
### Notes on the API
|
14
21
|
|
15
22
|
- ID vs. UID: they aren't used consistently across the docs and actual API responses.
|
data/bin/console
CHANGED
@@ -8,8 +8,9 @@ require 'typeform_data'
|
|
8
8
|
# with your gem easier. You can also use a different console, if you like.
|
9
9
|
|
10
10
|
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
-
require 'pry'
|
12
|
-
Pry.start
|
13
11
|
|
12
|
+
require 'pry'
|
14
13
|
require 'irb'
|
14
|
+
|
15
|
+
Pry.start
|
15
16
|
IRB.start
|
data/lib/typeform_data.rb
CHANGED
@@ -1,5 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module TypeformData
|
3
|
+
|
4
|
+
def self.dump(object)
|
5
|
+
Marshal.dump(object)
|
6
|
+
end
|
7
|
+
|
8
|
+
# Currently only handles single objects and arrays.
|
9
|
+
# @param [TypeformData::ValueClass]
|
10
|
+
# @param [TypeformData::Config]
|
11
|
+
def self.load(value_class_instance, config)
|
12
|
+
Marshal.load(value_class_instance).tap { |marshaled|
|
13
|
+
case marshaled
|
14
|
+
when Array
|
15
|
+
marshaled.each { |object| object.reconfig(config) }
|
16
|
+
else
|
17
|
+
marshaled.reconfig(config)
|
18
|
+
end
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
3
22
|
end
|
4
23
|
|
5
24
|
require 'net/http'
|
@@ -10,12 +29,16 @@ require 'json'
|
|
10
29
|
require 'typeform_data/version'
|
11
30
|
require 'typeform_data/client'
|
12
31
|
require 'typeform_data/errors'
|
32
|
+
require 'typeform_data/config'
|
33
|
+
require 'typeform_data/value_class'
|
13
34
|
|
14
35
|
require 'typeform_data/requestor'
|
15
|
-
require 'typeform_data/requestor/config'
|
16
36
|
require 'typeform_data/api_response'
|
17
37
|
|
18
38
|
require 'typeform_data/typeform'
|
39
|
+
require 'typeform_data/typeform/by_id'
|
19
40
|
require 'typeform_data/typeform/stats'
|
20
41
|
require 'typeform_data/typeform/response'
|
21
42
|
require 'typeform_data/typeform/question'
|
43
|
+
require 'typeform_data/typeform/answer'
|
44
|
+
require 'typeform_data/typeform/field'
|
data/lib/typeform_data/client.rb
CHANGED
@@ -4,18 +4,15 @@ require 'typeform_data/version'
|
|
4
4
|
module TypeformData
|
5
5
|
class Client
|
6
6
|
|
7
|
+
# For the sake of usability, we're breaking convention here and accepting an API key as the
|
8
|
+
# first parameter instead of an instance of TypeformData::Config.
|
7
9
|
def initialize(api_key:)
|
8
|
-
@
|
10
|
+
@config = TypeformData::Config.new(api_key: api_key)
|
9
11
|
end
|
10
12
|
|
11
|
-
def
|
12
|
-
|
13
|
-
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
def typeform(id)
|
18
|
-
::TypeformData::Typeform.new(self, id)
|
13
|
+
def self.new_from_config(config)
|
14
|
+
raise ArgumentError, 'Missing config' unless config
|
15
|
+
new(api_key: config.api_key)
|
19
16
|
end
|
20
17
|
|
21
18
|
# Your API key will automatically be added to the request URL as a query param, as required by
|
@@ -25,7 +22,17 @@ module TypeformData
|
|
25
22
|
# @param Hash
|
26
23
|
# @return TypeformData::ApiResponse
|
27
24
|
def get(endpoint, params = {})
|
28
|
-
|
25
|
+
TypeformData::Requestor.get(@config, endpoint, params)
|
26
|
+
end
|
27
|
+
|
28
|
+
def all_typeforms
|
29
|
+
get('forms').parsed_json.map do |form_hash|
|
30
|
+
::TypeformData::Typeform.new(@config, form_hash)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def typeform(id)
|
35
|
+
::TypeformData::Typeform.new(@config, id: id)
|
29
36
|
end
|
30
37
|
|
31
38
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module TypeformData
|
3
|
+
class Config
|
4
|
+
attr_reader :api_key
|
5
|
+
|
6
|
+
def initialize(api_key:)
|
7
|
+
unless api_key.is_a?(String) && api_key.length.positive?
|
8
|
+
raise ArgumentError, 'An API key (as a nonempty String) is required'
|
9
|
+
end
|
10
|
+
@api_key = api_key
|
11
|
+
end
|
12
|
+
|
13
|
+
# These values were determined via URI.parse('https://api.typeform.com').
|
14
|
+
|
15
|
+
def host
|
16
|
+
'api.typeform.com'
|
17
|
+
end
|
18
|
+
|
19
|
+
def port
|
20
|
+
443
|
21
|
+
end
|
22
|
+
|
23
|
+
def api_version
|
24
|
+
1
|
25
|
+
end
|
26
|
+
|
27
|
+
def ==(other)
|
28
|
+
other.api_key == api_key
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
data/lib/typeform_data/errors.rb
CHANGED
@@ -1,29 +1,25 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module TypeformData
|
4
|
-
|
4
|
+
module Requestor
|
5
5
|
|
6
|
-
def
|
7
|
-
|
6
|
+
def self.get(config, endpoint, params = nil)
|
7
|
+
request(config, Net::HTTP::Get, request_path(config, endpoint), params)
|
8
8
|
end
|
9
9
|
|
10
|
-
def
|
11
|
-
|
10
|
+
def self.request_path(config, endpoint)
|
11
|
+
"/v#{config.api_version}/#{endpoint}"
|
12
12
|
end
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
def request_path(endpoint)
|
17
|
-
"/v#{@config.api_version}/#{endpoint}"
|
18
|
-
end
|
14
|
+
private_class_method :request_path
|
19
15
|
|
20
16
|
# rubocop:disable Metrics/MethodLength
|
21
17
|
# @return TypeformData::ApiResponse
|
22
|
-
def request(method_class, path, input_params = {})
|
18
|
+
def self.request(config, method_class, path, input_params = {})
|
23
19
|
params = input_params.dup
|
24
|
-
params[:key] =
|
20
|
+
params[:key] = config.api_key
|
25
21
|
|
26
|
-
response = Net::HTTP.new(
|
22
|
+
response = Net::HTTP.new(config.host, config.port).tap { |http|
|
27
23
|
http.use_ssl = true
|
28
24
|
|
29
25
|
# Uncomment this line for debugging:
|
@@ -37,23 +33,25 @@ module TypeformData
|
|
37
33
|
|
38
34
|
case response
|
39
35
|
when Net::HTTPNotFound then
|
40
|
-
raise
|
36
|
+
raise TypeformData::InvalidEndpointOrMissingResource, path
|
41
37
|
when Net::HTTPForbidden then
|
42
|
-
raise
|
38
|
+
raise TypeformData::InvalidApiKey, "Invalid api key: #{config.api_key}"
|
43
39
|
when Net::HTTPBadRequest then
|
44
|
-
raise
|
40
|
+
raise TypeformData::BadRequest, 'There was an error processing your request: '\
|
45
41
|
"#{response.body}, with params: #{params}"
|
46
42
|
when Net::HTTPSuccess
|
47
43
|
return TypeformData::ApiResponse.new(response)
|
48
44
|
else
|
49
|
-
raise
|
45
|
+
raise TypeformData::UnexpectedError, "A #{response.code} error has occurred: "\
|
50
46
|
"'#{response.message}'"
|
51
47
|
end
|
52
48
|
|
53
49
|
rescue Errno::ECONNREFUSED
|
54
|
-
raise
|
50
|
+
raise TypeformData::ConnectionRefused, 'The connection was refused'
|
55
51
|
end
|
56
52
|
# rubocop:enable Metrics/MethodLength
|
57
53
|
|
54
|
+
private_class_method :request
|
55
|
+
|
58
56
|
end
|
59
57
|
end
|
@@ -2,43 +2,12 @@
|
|
2
2
|
require 'typeform_data/version'
|
3
3
|
|
4
4
|
module TypeformData
|
5
|
-
class Typeform
|
6
|
-
attr_reader :id
|
7
|
-
attr_reader :name
|
8
|
-
|
9
|
-
MAX_PAGE_SIZE = 1000 # This is documented at https://www.typeform.com/help/data-api/.
|
10
|
-
|
11
|
-
# @param TypeformData::Client
|
12
|
-
# @param Hash<[String, Symbol], String>]
|
13
|
-
def initialize(client, attrs)
|
14
|
-
input_id = attrs['id'] || attrs[:id]
|
15
|
-
name = attrs['name'] || attrs[:name]
|
16
|
-
|
17
|
-
unless client.is_a?(::TypeformData::Client)
|
18
|
-
raise TypeformData::Error, 'Expected to receive a TypeformData::Client'
|
19
|
-
end
|
20
5
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
str_id = input_id.to_s
|
25
|
-
rescue NoMethodError
|
26
|
-
raise TypeformData::Error, "The provided ID is not a String, or can't be converted to one."
|
27
|
-
end
|
28
|
-
|
29
|
-
@id = str_id
|
30
|
-
@name = name if name
|
31
|
-
@client = client
|
32
|
-
end
|
6
|
+
class Typeform
|
7
|
+
include TypeformData::ValueClass
|
8
|
+
readable_attributes :id, :name
|
33
9
|
|
34
|
-
|
35
|
-
'completed' => Object,
|
36
|
-
'since' => Object,
|
37
|
-
'until' => Object,
|
38
|
-
'offset' => Fixnum,
|
39
|
-
'limit' => Fixnum,
|
40
|
-
'token' => String,
|
41
|
-
}.freeze
|
10
|
+
# TODO: define comparison methods <=>, etc.
|
42
11
|
|
43
12
|
# See https://www.typeform.com/help/data-api/ under "Filtering Options" for the full list of
|
44
13
|
# options.
|
@@ -58,33 +27,59 @@ module TypeformData
|
|
58
27
|
# handle the awkwardness of returning multiple kinds of data at the same time.
|
59
28
|
response = responses_request(collapse_and_validate_responses_params(params))
|
60
29
|
set_stats(response['stats']['responses'])
|
30
|
+
|
31
|
+
# It's important that we set the questions first, since the Answer constructor (called
|
32
|
+
# inside the Response constructor) looks up and denormalizes the question text.
|
61
33
|
set_questions(response['questions'])
|
62
34
|
|
63
|
-
response['responses'].map { |
|
64
|
-
Response.
|
35
|
+
response['responses'].map { |api_hash|
|
36
|
+
Response.new(config, api_hash.dup.tap { |hash| hash[:typeform_id] = id }, fields)
|
65
37
|
}
|
66
38
|
end
|
67
39
|
|
40
|
+
def fields
|
41
|
+
@_fields ||= Field.from_questions(config, questions)
|
42
|
+
end
|
43
|
+
|
44
|
+
# In general, Typeform's "question" concept is less useful than the field concept. TODO: add
|
45
|
+
# more notes on this.
|
68
46
|
def questions
|
69
|
-
@_questions ||= fetch_questions
|
47
|
+
(@_questions ||= fetch_questions).reject(&:hidden_field?)
|
48
|
+
end
|
49
|
+
|
50
|
+
def hidden_fields
|
51
|
+
(@_questions ||= fetch_questions).select(&:hidden_field?)
|
70
52
|
end
|
71
53
|
|
72
54
|
def stats
|
73
55
|
@_stats ||= fetch_stats
|
74
56
|
end
|
75
57
|
|
58
|
+
def ==(other)
|
59
|
+
other.id == id && other.config == config
|
60
|
+
end
|
61
|
+
|
62
|
+
def name
|
63
|
+
return name if name
|
64
|
+
@name ||= client.all_typeforms.find { |typeform| typeform.id == id }.name
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
76
69
|
def fetch_questions
|
77
|
-
questions = responses_request(limit: 1)
|
78
|
-
questions.map { |hash| Question.
|
70
|
+
questions = responses_request(limit: 1)['questions'] || []
|
71
|
+
questions.map { |hash| Question.new(config, hash) }
|
79
72
|
end
|
80
73
|
|
81
74
|
def fetch_stats
|
82
|
-
|
83
|
-
Stats.
|
75
|
+
hash = responses_request(limit: 1)['stats']['responses']
|
76
|
+
Stats.new(config, hash)
|
84
77
|
end
|
85
78
|
|
79
|
+
MAX_PAGE_SIZE = 1000 # This is documented at https://www.typeform.com/help/data-api/.
|
80
|
+
|
86
81
|
ResponsesRequest = Struct.new(:params, :response) do
|
87
|
-
#
|
82
|
+
# The inverse of 'do we need at least one more request to get all the data we want?'
|
88
83
|
# @return Boolean
|
89
84
|
def last_request?
|
90
85
|
params['limit'] || responses_count < MAX_PAGE_SIZE
|
@@ -95,22 +90,17 @@ module TypeformData
|
|
95
90
|
end
|
96
91
|
end
|
97
92
|
|
98
|
-
# TODO: make this private once the implementation is solid.
|
99
|
-
#
|
100
93
|
# It looks like sometimes the Typeform API will report stats that are out-of-date relative to
|
101
|
-
# the responses it actually returns.
|
94
|
+
# the responses it actually returns. TODO: look into this more.
|
102
95
|
def responses_request(input_params = {})
|
103
96
|
params = input_params.dup
|
104
|
-
requests = [ResponsesRequest.new(params,
|
97
|
+
requests = [ResponsesRequest.new(params, client.get('form/' + id, params))]
|
105
98
|
|
106
99
|
loop do
|
107
|
-
if requests.last.last_request?
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
next_params['offset'] += requests.last.responses_count
|
112
|
-
requests << ResponsesRequest.new(next_params, @client.get('form/' + id, next_params))
|
113
|
-
end
|
100
|
+
break if requests.last.last_request?
|
101
|
+
next_params = requests.last.params.dup
|
102
|
+
next_params['offset'] += requests.last.responses_count
|
103
|
+
requests << ResponsesRequest.new(next_params, client.get('form/' + id, next_params))
|
114
104
|
end
|
115
105
|
|
116
106
|
requests.map(&:response).map(&:parsed_json).reduce do |combined, next_set|
|
@@ -120,26 +110,33 @@ module TypeformData
|
|
120
110
|
end
|
121
111
|
end
|
122
112
|
|
123
|
-
private
|
124
|
-
|
125
113
|
# rubocop:disable Style/AccessorMethodName
|
126
114
|
def set_questions(questions_hashes = [])
|
127
|
-
@_questions = questions_hashes.map { |hash| Question.
|
115
|
+
@_questions = questions_hashes.map { |hash| Question.new(config, hash) }
|
128
116
|
end
|
129
117
|
|
130
118
|
# @param Hash stats_hash of the form {"responses"=>{"showing"=>2, "total"=>2, "completed"=>0}}
|
131
|
-
def set_stats(
|
132
|
-
@_stats = Stats.
|
119
|
+
def set_stats(hash)
|
120
|
+
@_stats = Stats.new(config, hash)
|
133
121
|
end
|
134
122
|
# rubocop:enable Style/AccessorMethodName
|
135
123
|
|
124
|
+
PERMITTED_KEYS = {
|
125
|
+
'completed' => Object,
|
126
|
+
'since' => Object,
|
127
|
+
'until' => Object,
|
128
|
+
'offset' => Fixnum,
|
129
|
+
'limit' => Fixnum,
|
130
|
+
'token' => String,
|
131
|
+
}.freeze
|
132
|
+
|
136
133
|
def collapse_and_validate_responses_params(input_params)
|
137
134
|
params = input_params.dup
|
138
135
|
|
139
136
|
params.keys.select { |key| key.is_a?(Symbol) }.each do |sym|
|
140
137
|
raise ::TypeformData::ArgumentError, 'Duplicate keys' if params.key?(sym.to_s)
|
141
|
-
params[sym.to_s] = params[
|
142
|
-
params.delete(
|
138
|
+
params[sym.to_s] = params[sym]
|
139
|
+
params.delete(sym)
|
143
140
|
end
|
144
141
|
|
145
142
|
params.keys.each do |key|
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module TypeformData
|
3
|
+
class Typeform
|
4
|
+
|
5
|
+
class Answer
|
6
|
+
include TypeformData::ValueClass
|
7
|
+
include TypeformData::Typeform::ById
|
8
|
+
|
9
|
+
# field_text may be removed in the future: we may want to normalize our data model. For
|
10
|
+
# now, it's quite convenient to have.
|
11
|
+
readable_attributes :id, :value, :field_text, :response_token, :typeform_id
|
12
|
+
|
13
|
+
# IDs are of the form:
|
14
|
+
#
|
15
|
+
# - "textfield_12316024"
|
16
|
+
# - "listimage_12316029_choice_12322262"
|
17
|
+
#
|
18
|
+
# This list may not be exhaustive-- there may be other ID formats not covererd above-- since
|
19
|
+
# this part of the API isn't mentioned in the documentation.
|
20
|
+
def question_field_id
|
21
|
+
id.split('_')[1]
|
22
|
+
end
|
23
|
+
|
24
|
+
def question_type
|
25
|
+
id.split('_').first
|
26
|
+
end
|
27
|
+
|
28
|
+
# TODO: this is not working-- Typeform is giving us back a 404. Perhaps it's an encoding
|
29
|
+
# issue with the token?
|
30
|
+
# def response
|
31
|
+
# typeform.responses(token: response_token)
|
32
|
+
# end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module TypeformData
|
3
|
+
class Typeform
|
4
|
+
|
5
|
+
class Field
|
6
|
+
include TypeformData::ValueClass
|
7
|
+
include TypeformData::Typeform::ById
|
8
|
+
readable_attributes :id, :text, :typeform_id, :question_ids
|
9
|
+
|
10
|
+
# The Data API includes 'statements' as part of a Typeform's "questions", despite the fact
|
11
|
+
# that these statements don't have associated answers.
|
12
|
+
def statement?
|
13
|
+
id.split('_').first == 'statement'
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.from_questions(config, input_questions)
|
17
|
+
input_questions.group_by(&:field_id).map do |field_id, questions|
|
18
|
+
unless questions.map(&:text).uniq.length == 1
|
19
|
+
raise UnexpectedError, 'Expected question text to be the same based on field_id'
|
20
|
+
end
|
21
|
+
|
22
|
+
new(config, id: field_id, text: questions.first.text, question_ids: questions.map(&:id),
|
23
|
+
typeform_id: questions.first.typeform_id)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -3,24 +3,21 @@ module TypeformData
|
|
3
3
|
class Typeform
|
4
4
|
|
5
5
|
class Question
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
include TypeformData::ValueClass
|
7
|
+
include TypeformData::Typeform::ById
|
8
|
+
readable_attributes :id, :question, :field_id, :typeform_id
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
10
|
+
# Question#question makes for a bad API. Ideally, use Question#text instead.
|
11
|
+
alias text question
|
12
|
+
|
13
|
+
def hidden_field?
|
14
|
+
id.split('_').first == 'hidden'
|
15
15
|
end
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
question: hash['question'],
|
22
|
-
field_id: hash['field_id'],
|
23
|
-
)
|
17
|
+
# The Data API includes 'statements' as part of a Typeform's "questions", despite the fact
|
18
|
+
# that these statements don't have associated answers.
|
19
|
+
def statement?
|
20
|
+
id.split('_').first == 'statement'
|
24
21
|
end
|
25
22
|
end
|
26
23
|
|
@@ -3,34 +3,52 @@ module TypeformData
|
|
3
3
|
class Typeform
|
4
4
|
|
5
5
|
class Response
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
attr_reader :hidden
|
6
|
+
include TypeformData::ValueClass
|
7
|
+
include TypeformData::Typeform::ById
|
8
|
+
readable_attributes :token, :metadata, :hidden, :typeform_id, :answers, :completed
|
10
9
|
|
10
|
+
# It's correct to name this attribute "completed?" and not "complete?" since it's always in
|
11
|
+
# the past tense-- once a potential respondent leaves a Typeform unsubmitted, they can never
|
12
|
+
# go back and complete it.
|
11
13
|
def completed?
|
12
14
|
@completed == 1
|
13
15
|
end
|
14
16
|
|
15
17
|
alias hidden_fields hidden
|
16
18
|
|
17
|
-
def
|
18
|
-
|
19
|
-
@token = token
|
20
|
-
@metadata = metadata
|
21
|
-
@hidden = hidden
|
22
|
-
@completed = completed
|
19
|
+
def date_submitted
|
20
|
+
DateTime.strptime(metadata['date_submit'], '%Y-%m-%d %H:%M:%S')
|
23
21
|
end
|
24
22
|
|
25
|
-
def
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
23
|
+
def initialize(config, attrs, fields)
|
24
|
+
mapped_attrs = attrs.dup
|
25
|
+
|
26
|
+
mapped_attrs[:answers] = (attrs[:answers] || attrs['answers']).map { |id, value|
|
27
|
+
matching_field = fields.find { |field| field.id.to_s == id.split('_')[1] }
|
28
|
+
raise UnexpectedError, 'Expected to find a matching field' unless matching_field
|
29
|
+
|
30
|
+
Answer.new(
|
31
|
+
config,
|
32
|
+
id: id,
|
33
|
+
value: value,
|
34
|
+
field_text: matching_field.text,
|
35
|
+
response_token: attrs[:token] || attrs['token'],
|
36
|
+
typeform_id: attrs[:typeform_id] || attrs['typeform_id'],
|
37
|
+
)
|
38
|
+
}
|
39
|
+
|
40
|
+
super(config, mapped_attrs)
|
41
|
+
end
|
42
|
+
|
43
|
+
def reconfig(config)
|
44
|
+
@config = config
|
45
|
+
answers.each { |answer| answer.config = config }
|
46
|
+
end
|
47
|
+
|
48
|
+
def ==(other)
|
49
|
+
other.token == token && other.config == config
|
33
50
|
end
|
51
|
+
|
34
52
|
end
|
35
53
|
|
36
54
|
end
|
@@ -3,23 +3,8 @@ module TypeformData
|
|
3
3
|
class Typeform
|
4
4
|
|
5
5
|
class Stats
|
6
|
-
|
7
|
-
|
8
|
-
attr_reader :completed
|
9
|
-
|
10
|
-
def initialize(showing:, total:, completed:)
|
11
|
-
@showing = showing
|
12
|
-
@total = total
|
13
|
-
@completed = completed
|
14
|
-
end
|
15
|
-
|
16
|
-
def self.from_stats_hash(stats_hash)
|
17
|
-
new(
|
18
|
-
showing: stats_hash['showing'],
|
19
|
-
total: stats_hash['total'],
|
20
|
-
completed: stats_hash['completed'],
|
21
|
-
)
|
22
|
-
end
|
6
|
+
include TypeformData::ValueClass
|
7
|
+
readable_attributes :showing, :total, :completed
|
23
8
|
end
|
24
9
|
|
25
10
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module TypeformData
|
5
|
+
module ValueClass
|
6
|
+
|
7
|
+
def initialize(config, attrs)
|
8
|
+
unless config && config.is_a?(TypeformData::Config)
|
9
|
+
raise ArgumentError, 'Expected a TypeformData::Config instance as the first argument'
|
10
|
+
end
|
11
|
+
@config = config
|
12
|
+
|
13
|
+
keys = attribute_keys
|
14
|
+
|
15
|
+
attrs.each do |key, value|
|
16
|
+
unless keys.include?(key) || keys.include?(key.to_sym)
|
17
|
+
raise ArgumentError, "Unexpected key: #{key}"
|
18
|
+
end
|
19
|
+
instance_variable_set("@#{key}", value)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.included(base)
|
24
|
+
base.extend(ClassMethods)
|
25
|
+
end
|
26
|
+
|
27
|
+
module ClassMethods
|
28
|
+
# @param [Array<Symbol>]
|
29
|
+
def readable_attributes(*keys)
|
30
|
+
@keys = Set.new(keys)
|
31
|
+
|
32
|
+
keys.each do |key|
|
33
|
+
attr_reader(key)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
def marshal_dump
|
40
|
+
# For the sake of security, we don't want to serialize our API key.
|
41
|
+
attribute_keys.to_a.map do |key|
|
42
|
+
[key, instance_variable_get("@#{key}")]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def marshal_load(hash)
|
47
|
+
hash.each do |key, value|
|
48
|
+
instance_variable_set("@#{key}", value)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Compond classes (e.g. a Response which has many Answers) should use this method to re-set
|
53
|
+
# 'config' on each child object. ValueClass#reconfig is called in TypeformData#load.
|
54
|
+
def reconfig(config)
|
55
|
+
self.config = config
|
56
|
+
end
|
57
|
+
|
58
|
+
protected
|
59
|
+
|
60
|
+
attr_accessor :config
|
61
|
+
|
62
|
+
def client
|
63
|
+
TypeformData::Client.new_from_config(@config)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def attribute_keys
|
69
|
+
self.class.instance_eval { @keys } || []
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
data/typeform_data.gemspec
CHANGED
@@ -31,11 +31,12 @@ Gem::Specification.new do |spec|
|
|
31
31
|
spec.add_development_dependency 'bundler', '~> 1.12'
|
32
32
|
spec.add_development_dependency 'rake', '~> 10.0'
|
33
33
|
spec.add_development_dependency 'minitest', '~> 5.0'
|
34
|
+
spec.add_development_dependency 'flexmock', '~> 2.0'
|
34
35
|
|
35
|
-
spec.add_development_dependency 'pry', '0.10'
|
36
|
-
spec.add_development_dependency 'pry-byebug', '3.3'
|
37
|
-
spec.add_development_dependency 'pry-doc', '0.8'
|
38
|
-
spec.add_development_dependency 'byebug', '8.2'
|
39
|
-
spec.add_development_dependency 'rubocop', '0.39'
|
36
|
+
spec.add_development_dependency 'pry', '~> 0.10'
|
37
|
+
spec.add_development_dependency 'pry-byebug', '~> 3.3'
|
38
|
+
spec.add_development_dependency 'pry-doc', '~> 0.8'
|
39
|
+
spec.add_development_dependency 'byebug', '~> 8.2'
|
40
|
+
spec.add_development_dependency 'rubocop', '~> 0.39'
|
40
41
|
|
41
42
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: typeform_data
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Max Wallace
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-08-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -52,74 +52,88 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '5.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: flexmock
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '2.0'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: pry
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
58
72
|
requirements:
|
59
|
-
- -
|
73
|
+
- - "~>"
|
60
74
|
- !ruby/object:Gem::Version
|
61
75
|
version: '0.10'
|
62
76
|
type: :development
|
63
77
|
prerelease: false
|
64
78
|
version_requirements: !ruby/object:Gem::Requirement
|
65
79
|
requirements:
|
66
|
-
- -
|
80
|
+
- - "~>"
|
67
81
|
- !ruby/object:Gem::Version
|
68
82
|
version: '0.10'
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: pry-byebug
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
72
86
|
requirements:
|
73
|
-
- -
|
87
|
+
- - "~>"
|
74
88
|
- !ruby/object:Gem::Version
|
75
89
|
version: '3.3'
|
76
90
|
type: :development
|
77
91
|
prerelease: false
|
78
92
|
version_requirements: !ruby/object:Gem::Requirement
|
79
93
|
requirements:
|
80
|
-
- -
|
94
|
+
- - "~>"
|
81
95
|
- !ruby/object:Gem::Version
|
82
96
|
version: '3.3'
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
98
|
name: pry-doc
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
86
100
|
requirements:
|
87
|
-
- -
|
101
|
+
- - "~>"
|
88
102
|
- !ruby/object:Gem::Version
|
89
103
|
version: '0.8'
|
90
104
|
type: :development
|
91
105
|
prerelease: false
|
92
106
|
version_requirements: !ruby/object:Gem::Requirement
|
93
107
|
requirements:
|
94
|
-
- -
|
108
|
+
- - "~>"
|
95
109
|
- !ruby/object:Gem::Version
|
96
110
|
version: '0.8'
|
97
111
|
- !ruby/object:Gem::Dependency
|
98
112
|
name: byebug
|
99
113
|
requirement: !ruby/object:Gem::Requirement
|
100
114
|
requirements:
|
101
|
-
- -
|
115
|
+
- - "~>"
|
102
116
|
- !ruby/object:Gem::Version
|
103
117
|
version: '8.2'
|
104
118
|
type: :development
|
105
119
|
prerelease: false
|
106
120
|
version_requirements: !ruby/object:Gem::Requirement
|
107
121
|
requirements:
|
108
|
-
- -
|
122
|
+
- - "~>"
|
109
123
|
- !ruby/object:Gem::Version
|
110
124
|
version: '8.2'
|
111
125
|
- !ruby/object:Gem::Dependency
|
112
126
|
name: rubocop
|
113
127
|
requirement: !ruby/object:Gem::Requirement
|
114
128
|
requirements:
|
115
|
-
- -
|
129
|
+
- - "~>"
|
116
130
|
- !ruby/object:Gem::Version
|
117
131
|
version: '0.39'
|
118
132
|
type: :development
|
119
133
|
prerelease: false
|
120
134
|
version_requirements: !ruby/object:Gem::Requirement
|
121
135
|
requirements:
|
122
|
-
- -
|
136
|
+
- - "~>"
|
123
137
|
- !ruby/object:Gem::Version
|
124
138
|
version: '0.39'
|
125
139
|
description: typeform_data is a minimal, opinionated client for the Typeform.com Data
|
@@ -146,13 +160,17 @@ files:
|
|
146
160
|
- lib/typeform_data.rb
|
147
161
|
- lib/typeform_data/api_response.rb
|
148
162
|
- lib/typeform_data/client.rb
|
163
|
+
- lib/typeform_data/config.rb
|
149
164
|
- lib/typeform_data/errors.rb
|
150
165
|
- lib/typeform_data/requestor.rb
|
151
|
-
- lib/typeform_data/requestor/config.rb
|
152
166
|
- lib/typeform_data/typeform.rb
|
167
|
+
- lib/typeform_data/typeform/answer.rb
|
168
|
+
- lib/typeform_data/typeform/by_id.rb
|
169
|
+
- lib/typeform_data/typeform/field.rb
|
153
170
|
- lib/typeform_data/typeform/question.rb
|
154
171
|
- lib/typeform_data/typeform/response.rb
|
155
172
|
- lib/typeform_data/typeform/stats.rb
|
173
|
+
- lib/typeform_data/value_class.rb
|
156
174
|
- lib/typeform_data/version.rb
|
157
175
|
- typeform_data.gemspec
|
158
176
|
homepage: https://github.com/shearwater-intl/typeform_data
|
@@ -180,4 +198,3 @@ signing_key:
|
|
180
198
|
specification_version: 4
|
181
199
|
summary: An opinionated client for the Typeform.com Data API
|
182
200
|
test_files: []
|
183
|
-
has_rdoc:
|
@@ -1,29 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
module TypeformData
|
3
|
-
class Requestor
|
4
|
-
|
5
|
-
class Config
|
6
|
-
attr_reader :api_key
|
7
|
-
|
8
|
-
def initialize(api_key:)
|
9
|
-
@api_key = api_key
|
10
|
-
end
|
11
|
-
|
12
|
-
# These values were determined via URI.parse('https://api.typeform.com')
|
13
|
-
|
14
|
-
def host
|
15
|
-
'api.typeform.com'
|
16
|
-
end
|
17
|
-
|
18
|
-
def port
|
19
|
-
443
|
20
|
-
end
|
21
|
-
|
22
|
-
def api_version
|
23
|
-
1
|
24
|
-
end
|
25
|
-
|
26
|
-
end
|
27
|
-
|
28
|
-
end
|
29
|
-
end
|