typed_form 0.0.4 → 0.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +11 -7
- data/lib/typed_form/api/client.rb +53 -0
- data/lib/typed_form/form.rb +62 -0
- data/lib/typed_form/form_data/answers.rb +91 -0
- data/lib/typed_form/form_data/form_submission.rb +53 -0
- data/lib/typed_form/form_data/parsed_json.rb +29 -0
- data/lib/typed_form/form_data/question.rb +62 -0
- data/lib/typed_form/version.rb +2 -1
- data/lib/typed_form/webhook.rb +47 -0
- data/lib/typed_form.rb +9 -5
- metadata +9 -7
- data/lib/typed_form/client.rb +0 -35
- data/lib/typed_form/form_answers.rb +0 -66
- data/lib/typed_form/form_response.rb +0 -52
- data/lib/typed_form/json_response_handler.rb +0 -16
- data/lib/typed_form/question.rb +0 -42
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d67800ceba46ef713f9fc5aa6d60390ad29d1737
|
4
|
+
data.tar.gz: dfe2b5bcf4c885ad9883af07d6a92f86d107572c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3817ca0f77392d2846dca9b7f7b51fc00a5685d3caacf5a25dd90f6ec7a6a4aefa9cffa92a5d0eb8218b05099234ea14af6631512463b8edf9a8c7ee08d94b01
|
7
|
+
data.tar.gz: c1ed19b2752ec0e4d3fed9a2a20a913ba751c18bbf7af79ec79f359a070b4a8d6792f559f2fc65241a7cdbe3e96a7bc9a563877d1c9dad1f04fa9876d072b3af
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## [0.1.0] - 2017-03-12
|
4
|
+
|
5
|
+
Adds logic for extracting data needed for Data API from incoming Webhooks.
|
6
|
+
|
7
|
+
Adds YARD documentation to gem.
|
8
|
+
|
9
|
+
Separates API/Client, Data-API Form Data, and Form/Webhook responsibilities into isolated namespaces. Stabilizes Gem API around accessing form data via
|
10
|
+
API and loading from existing JSON data sources.
|
11
|
+
|
3
12
|
## [0.0.4] - 2017-03-10
|
4
13
|
|
5
14
|
Defaults to freezing Questions after initialization, to prevent concerns with mutating data inadvertently when working with questions as value objects.
|
data/README.md
CHANGED
@@ -78,13 +78,15 @@ form_id = "typeform_form_id"
|
|
78
78
|
# individual response token, provided in responses hash or via webhook data
|
79
79
|
token = "form_token"
|
80
80
|
|
81
|
-
client = TypedForm::Client.new(api_key: api_key)
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
81
|
+
client = TypedForm::API::Client.new(api_key: api_key)
|
82
|
+
form = TypedForm::Form.find_form_by(
|
83
|
+
client: client,
|
84
|
+
form_id: form_id,
|
85
|
+
token: token
|
86
|
+
)
|
87
|
+
|
88
|
+
# Or, load from existing JSON
|
89
|
+
form = TypedForm::Form.build_form_from(json: your_json_source)
|
88
90
|
questions = form.questions
|
89
91
|
|
90
92
|
questions.first.text
|
@@ -103,6 +105,8 @@ questions.first.type
|
|
103
105
|
|
104
106
|
The most common use case for this is extrapolating question and answers into a simple object that can provide a clean interface for displaying them. Question type information can be used to allow helpers to format and display different field types (most specifically dates) in a more user-friendly format.
|
105
107
|
|
108
|
+
Additional documentation is available at [Rubydoc.](http://www.rubydoc.info/github/useed/typed_form)
|
109
|
+
|
106
110
|
## Development
|
107
111
|
|
108
112
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module TypedForm
|
2
|
+
# Objects related to working with the Typeform API.
|
3
|
+
module API
|
4
|
+
# API wrapper for querying typeform's Data API.
|
5
|
+
# @attr [String] api_key Your Typeform API key.
|
6
|
+
# @see https://www.typeform.com/help/data-api/ Typeform Data API docs
|
7
|
+
class Client
|
8
|
+
attr_reader :api_key
|
9
|
+
|
10
|
+
include HTTParty
|
11
|
+
|
12
|
+
# Creates a new instance of an API client for querying the
|
13
|
+
# Typeform Data API.
|
14
|
+
# @param [String] api_key Typeform API Key
|
15
|
+
def initialize(api_key:)
|
16
|
+
@api_key = api_key
|
17
|
+
end
|
18
|
+
|
19
|
+
# Queries the Typeform Data API /form/ endpoint to retrieve a specific
|
20
|
+
# response (filtered by token) for a specific Form.
|
21
|
+
#
|
22
|
+
# @param [String] form_id Typeform Form ID
|
23
|
+
# @param [String] token The token for the form response you are querying
|
24
|
+
# @param [Hash<Object>] query_params Splats and passes along query
|
25
|
+
# parameters to the form's query. See
|
26
|
+
# https://www.typeform.com/help/data-api/ under Filtering Options for
|
27
|
+
# more information.
|
28
|
+
def find_form_by(form_id:, token:, **query_params)
|
29
|
+
forms_by_id(form_id: form_id, token: token, **query_params)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def forms_by_id(form_id:, **query_params)
|
35
|
+
url_params = query_params.map { |k, v| "#{k}=#{v}" }
|
36
|
+
request_url = [form_id, authenticated_slug(url_params)].join("?")
|
37
|
+
get(request_url).body
|
38
|
+
end
|
39
|
+
|
40
|
+
def get(slug)
|
41
|
+
self.class.get(base_url + slug)
|
42
|
+
end
|
43
|
+
|
44
|
+
def base_url
|
45
|
+
"https://api.typeform.com/v1/form/"
|
46
|
+
end
|
47
|
+
|
48
|
+
def authenticated_slug(url_params)
|
49
|
+
["key=#{api_key}", url_params].join("&")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module TypedForm
|
2
|
+
# A representation of the Typeform Form Data for a single
|
3
|
+
# form response.
|
4
|
+
#
|
5
|
+
# @attr_reader [String] json JSON data using Typeform's Data API schema
|
6
|
+
class Form
|
7
|
+
extend Forwardable
|
8
|
+
attr_accessor :json
|
9
|
+
|
10
|
+
# @!method responses
|
11
|
+
# @see FormData::ParsedJson#responses
|
12
|
+
def_delegators :parsed_json, :responses
|
13
|
+
|
14
|
+
# @!method questions
|
15
|
+
# @see FormSubmission#questions
|
16
|
+
def_delegators :submission, :questions
|
17
|
+
|
18
|
+
# Creates a new instance of a Form, to allow querying
|
19
|
+
def initialize(json: json)
|
20
|
+
@json = json
|
21
|
+
end
|
22
|
+
|
23
|
+
# Uses the Typeform API client to query/find the form based on the form_id
|
24
|
+
# and token, then builds a new Form from that JSON request.
|
25
|
+
# @param [API::Client] client Typeform API client instance
|
26
|
+
# @param [String] form_id Form ID you're querying
|
27
|
+
# @param [String] token The token for the response you're retrieving
|
28
|
+
# @return [Form] A Form, via JSON fetched from Typeform's API
|
29
|
+
def self.find_form_by(client:, form_id:, token:)
|
30
|
+
json = client.find_form_by(form_id: form_id, token: token)
|
31
|
+
new(json: json)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Builds a hash of Questions matched with Answers.
|
35
|
+
# @return [Hash] A Hash matching { "Question" => "Answer" } format.
|
36
|
+
def to_hash
|
37
|
+
questions.each_with_object({}) { |q, hash| hash[q.text] = q.answer }
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def submission
|
43
|
+
raise StandardError, "Form expects a single response" if multi_response?
|
44
|
+
@_submission ||= FormData::FormSubmission.new(
|
45
|
+
parsed_questions: parsed_json.questions,
|
46
|
+
parsed_response: responses.first
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
def multi_response?
|
51
|
+
responses.size > 1
|
52
|
+
end
|
53
|
+
|
54
|
+
def parsed_json
|
55
|
+
@_parsed_json ||= FormData::ParsedJson.new(json: json)
|
56
|
+
end
|
57
|
+
|
58
|
+
def response
|
59
|
+
responses.first
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module TypedForm
|
2
|
+
# A collection of Modules that build value objects around data from the
|
3
|
+
# Typeform Data API.
|
4
|
+
module FormData
|
5
|
+
# A collection class which takes a collection of answers to a form, and
|
6
|
+
# associates the questions with answers.
|
7
|
+
#
|
8
|
+
# @attr_reader [Arendelle] parsed_response An immutable object representing
|
9
|
+
# the submission data from the form.
|
10
|
+
# @attr_reader [Arendelle] parsed_questions An immutable object representing
|
11
|
+
# the question data from the form.
|
12
|
+
class Answers
|
13
|
+
extend Forwardable
|
14
|
+
|
15
|
+
# @!method answers
|
16
|
+
# @return [Arendelle] Parsed Questions from Typeform Data API JSON
|
17
|
+
# @!method metadata
|
18
|
+
# @return [Arendelle] Parsed Metadata from Typeform Data API JSON
|
19
|
+
# @!method token
|
20
|
+
# @return [String] Token extracted from Typeform Data API JSON
|
21
|
+
def_delegators :parsed_response, :answers, :metadata, :token
|
22
|
+
|
23
|
+
# @!method token
|
24
|
+
# @return [String] date form was submitted, in UTC
|
25
|
+
def_delegators :metadata, :date_submit
|
26
|
+
|
27
|
+
attr_reader :parsed_response, :input_questions, :parsed_questions
|
28
|
+
|
29
|
+
# Builds a collection of Questions, with text extrapolated to support
|
30
|
+
# "piped" questions (i.e. "What is the name of your {{answer_42}}").
|
31
|
+
#
|
32
|
+
# @return [Array<Question>] Question value objects with extrapolated
|
33
|
+
# text and answers.
|
34
|
+
def self.collate(parsed_response:, parsed_questions:, input_questions:)
|
35
|
+
new(parsed_response: parsed_response,
|
36
|
+
parsed_questions: parsed_questions,
|
37
|
+
input_questions: input_questions).questions
|
38
|
+
end
|
39
|
+
|
40
|
+
# Iterates through the existing collection of input questions to build a
|
41
|
+
# set of question value objects. Memoizes result.
|
42
|
+
# @return [Array<Question>]
|
43
|
+
def questions
|
44
|
+
@_questions ||= input_questions.map do |question|
|
45
|
+
Question.with_response_data(
|
46
|
+
question: question,
|
47
|
+
answer: answers_for(question.ids),
|
48
|
+
text: extrapolated_question_text(question)
|
49
|
+
)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
private_class_method :new
|
56
|
+
def initialize(parsed_response:, input_questions:, parsed_questions:)
|
57
|
+
@parsed_response = parsed_response
|
58
|
+
@input_questions = input_questions
|
59
|
+
@parsed_questions = parsed_questions
|
60
|
+
end
|
61
|
+
|
62
|
+
def answers_for(ids)
|
63
|
+
id_answers = ids.map { |id| find_answer_by_id(id) }.compact
|
64
|
+
return if id_answers.size.zero?
|
65
|
+
id_answers.join(", ")
|
66
|
+
end
|
67
|
+
|
68
|
+
def extrapolated_question_text(question)
|
69
|
+
regex = %r(\{\{answer_(\d+)\}\})
|
70
|
+
id_match = question.original_text.match(regex)
|
71
|
+
return question.original_text unless id_match
|
72
|
+
|
73
|
+
question.original_text.gsub(regex, find_answer_by_field_id(id_match[1]))
|
74
|
+
end
|
75
|
+
|
76
|
+
def find_answer_by_field_id(id)
|
77
|
+
fields = parsed_questions.select do |question|
|
78
|
+
question.field_id == id.to_i
|
79
|
+
end
|
80
|
+
|
81
|
+
answers_found = fields.map { |f| find_answer_by_id(f.id) }.compact
|
82
|
+
return find_answer_by_id(fields.first.id) if answers_found.size == 1
|
83
|
+
raise ArgumentError, "Cannot find single answer with field ID ##{id}"
|
84
|
+
end
|
85
|
+
|
86
|
+
def find_answer_by_id(id)
|
87
|
+
answers.instance_variable_get("@#{id}")
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module TypedForm
|
2
|
+
module FormData
|
3
|
+
# Takes an individual parsed response for a series of questions, and
|
4
|
+
# provides an interface for accessing the Question value objects.
|
5
|
+
#
|
6
|
+
# @attr_reader [Arendelle] parsed_questions Parsed Questions from JSON
|
7
|
+
# @attr_reader [Arendelle] parsed_response Parsed Answers from JSON
|
8
|
+
class FormSubmission
|
9
|
+
attr_reader :parsed_questions, :parsed_response
|
10
|
+
|
11
|
+
# Creates a new Form Submission.
|
12
|
+
#
|
13
|
+
# @param [Arendelle] parsed_questions Parsed Questions from JSON
|
14
|
+
# @param [Arendelle] parsed_response Parsed Answers from JSON
|
15
|
+
def initialize(parsed_questions:, parsed_response:)
|
16
|
+
@parsed_questions = parsed_questions
|
17
|
+
@parsed_response = parsed_response
|
18
|
+
end
|
19
|
+
|
20
|
+
# Builds a full set of Question value objects with answer text.
|
21
|
+
# @return [Array<Question>]
|
22
|
+
def questions
|
23
|
+
@_questions ||= Answers.collate(parsed_response: parsed_response,
|
24
|
+
parsed_questions: parsed_questions,
|
25
|
+
input_questions: input_questions)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def question_for_grouped(grouped_questions)
|
31
|
+
question_texts = grouped_questions.map(&:question)
|
32
|
+
return question_texts.first if question_texts.uniq.size == 1
|
33
|
+
raise ArgumentError, "Grouped questions do not have matching text"
|
34
|
+
end
|
35
|
+
|
36
|
+
def answerable_questions
|
37
|
+
parsed_questions
|
38
|
+
.reject { |q| q.id.match /(hidden|legal|statement|group)/ }
|
39
|
+
.group_by(&:field_id)
|
40
|
+
end
|
41
|
+
|
42
|
+
def input_questions
|
43
|
+
@_input_questions ||= answerable_questions.map do |field_id, grouped_q|
|
44
|
+
Question.new(
|
45
|
+
ids: grouped_q.map(&:id),
|
46
|
+
field_id: field_id,
|
47
|
+
original_text: question_for_grouped(grouped_q)
|
48
|
+
).freeze
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module TypedForm
|
2
|
+
module FormData
|
3
|
+
# A small class which wraps functionality for parsing JSON data from the
|
4
|
+
# Typeform Data API.
|
5
|
+
# @attr_reader [String] JSON string
|
6
|
+
class ParsedJson
|
7
|
+
extend Forwardable
|
8
|
+
attr_reader :json
|
9
|
+
|
10
|
+
# @!method questions
|
11
|
+
# @return [Arendelle] parsed_json["questions"] questions data
|
12
|
+
# @!method responses
|
13
|
+
# @return [Arendelle] parsed_json["responses"] response data
|
14
|
+
def_delegators :parsed_json, :questions, :responses
|
15
|
+
|
16
|
+
# Creates and freezes JSON data.
|
17
|
+
# @param [String] json JSON data matching the Typeform Data API format.
|
18
|
+
def initialize(json:)
|
19
|
+
@json = json.freeze
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def parsed_json
|
25
|
+
@_parsed_json ||= JSON.parse(json, object_class: Arendelle)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module TypedForm
|
2
|
+
module FormData
|
3
|
+
# Question value objects represent the Typeform concept of a question, but
|
4
|
+
# expose an interface for querying questions and answers.
|
5
|
+
#
|
6
|
+
# @attr_reader [Array<Strong>] ids The Typeform IDs for the question. This
|
7
|
+
# is ids instead of id because of the way Typeform represents multiple
|
8
|
+
# choice questions.
|
9
|
+
# @attr_reader [Integer] field_id The Typeform Field ID for the question
|
10
|
+
# @attr_reader [String] The original text for the question before answers
|
11
|
+
# are extrapolated back into the question.
|
12
|
+
# @attr [String] answer The answer of the question.
|
13
|
+
# @attr [Text] answer The extrapolated text for the question
|
14
|
+
class Question
|
15
|
+
attr_reader :ids, :field_id, :original_text
|
16
|
+
attr_accessor :answer, :text
|
17
|
+
|
18
|
+
# Creates a new Question value object, which represents any number of
|
19
|
+
# questions in a Typeform Form that can be logically represented as a
|
20
|
+
# single question. This includes both single question fields and fields
|
21
|
+
# like multiple choice, picture choice, etc.
|
22
|
+
#
|
23
|
+
# @param [Array<Strong>] ids The Typeform IDs for the question. This
|
24
|
+
# is ids instead of id because of the way Typeform represents multiple
|
25
|
+
# choice questions.
|
26
|
+
# @param [Integer] field_id The Typeform Field ID for the question
|
27
|
+
# @param [String] original_text The original text for the question before
|
28
|
+
# answers are extrapolated back into the question.
|
29
|
+
def initialize(ids:, field_id:, original_text:)
|
30
|
+
@ids = ids
|
31
|
+
@field_id = field_id
|
32
|
+
@original_text = original_text
|
33
|
+
end
|
34
|
+
|
35
|
+
# Performs a regular expression based on the id of the question, to
|
36
|
+
# determine the Type of object. This information can be queried in order
|
37
|
+
# to allow users to handle various types of Typeform data differently.
|
38
|
+
def type
|
39
|
+
@_type ||= determine_type
|
40
|
+
end
|
41
|
+
|
42
|
+
# Creates a new Question with existing data from a previous question.
|
43
|
+
#
|
44
|
+
# @return [Question] Question with extrapolated text in questions and
|
45
|
+
# with the answer to the question as an attribute.
|
46
|
+
def self.with_response_data(question:, text:, answer:)
|
47
|
+
question.dup.tap do |new_question|
|
48
|
+
new_question.answer = answer
|
49
|
+
new_question.text = text
|
50
|
+
end.freeze
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def determine_type
|
56
|
+
detected_type = ids.map { |id| id.split("_")[0] }.uniq
|
57
|
+
return detected_type.first if detected_type.size == 1
|
58
|
+
raise StandardError, "Cannot detect type of question ids #{ids}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/typed_form/version.rb
CHANGED
@@ -0,0 +1,47 @@
|
|
1
|
+
module TypedForm
|
2
|
+
# Methods used for handling incoming webhook events with data using the
|
3
|
+
# Typeform Webhook JSON schema.
|
4
|
+
#
|
5
|
+
# @see https://www.typeform.com/help/webhooks/ Typeform Webhook Docs
|
6
|
+
#
|
7
|
+
# @attr_reader [String] json JSON data from an incoming Typeform Webhook
|
8
|
+
class Webhook
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
# @!method form_response
|
12
|
+
# @return [Arendelle] An immutable representation of the Webhook JSON
|
13
|
+
# form_response field.
|
14
|
+
def_delegators :parsed_json, :form_response
|
15
|
+
|
16
|
+
# @!method form_id
|
17
|
+
# @return [String] The form ID from the webhook submission.
|
18
|
+
def_delegators :form_response, :form_id
|
19
|
+
attr_reader :json
|
20
|
+
|
21
|
+
# Creates a new webhook object from an incoming Typeform Data stream.
|
22
|
+
# @param [String] json JSON Data from a Typeform Webhook
|
23
|
+
def initialize(json: json)
|
24
|
+
@json = json.freeze
|
25
|
+
end
|
26
|
+
|
27
|
+
# Retrieves the Token from the Webhook JSON data.
|
28
|
+
#
|
29
|
+
# @return [String] Unique token for the form submission.
|
30
|
+
def form_token
|
31
|
+
form_response.token
|
32
|
+
end
|
33
|
+
|
34
|
+
# Retrieves the Form ID from the Webhook JSON data.
|
35
|
+
#
|
36
|
+
# @return [Integer] Typeform Form ID for the Webhook.
|
37
|
+
def form_id
|
38
|
+
form_response.form_id
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def parsed_json
|
44
|
+
@_parsed_json ||= JSON.parse(json, object_class: Arendelle)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/typed_form.rb
CHANGED
@@ -1,12 +1,16 @@
|
|
1
1
|
require "httparty"
|
2
2
|
require "forwardable"
|
3
3
|
require "arendelle"
|
4
|
-
require "typed_form/client"
|
5
|
-
require "typed_form/
|
6
|
-
require "typed_form/
|
7
|
-
require "typed_form/
|
8
|
-
require "typed_form/
|
4
|
+
require "typed_form/api/client"
|
5
|
+
require "typed_form/form_data/parsed_json"
|
6
|
+
require "typed_form/form_data/question"
|
7
|
+
require "typed_form/form_data/answers"
|
8
|
+
require "typed_form/form_data/form_submission"
|
9
|
+
require "typed_form/form"
|
10
|
+
require "typed_form/webhook"
|
9
11
|
require "typed_form/version"
|
10
12
|
|
13
|
+
# A collection of value objects and utilities for working with data fetched
|
14
|
+
# or cached from the Typeform Data API.
|
11
15
|
module TypedForm
|
12
16
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: typed_form
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rob Cole
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2017-03-
|
12
|
+
date: 2017-03-12 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: httparty
|
@@ -134,12 +134,14 @@ files:
|
|
134
134
|
- bin/console
|
135
135
|
- bin/setup
|
136
136
|
- lib/typed_form.rb
|
137
|
-
- lib/typed_form/client.rb
|
138
|
-
- lib/typed_form/
|
139
|
-
- lib/typed_form/
|
140
|
-
- lib/typed_form/
|
141
|
-
- lib/typed_form/
|
137
|
+
- lib/typed_form/api/client.rb
|
138
|
+
- lib/typed_form/form.rb
|
139
|
+
- lib/typed_form/form_data/answers.rb
|
140
|
+
- lib/typed_form/form_data/form_submission.rb
|
141
|
+
- lib/typed_form/form_data/parsed_json.rb
|
142
|
+
- lib/typed_form/form_data/question.rb
|
142
143
|
- lib/typed_form/version.rb
|
144
|
+
- lib/typed_form/webhook.rb
|
143
145
|
homepage: https://github.com/useed/typed_form
|
144
146
|
licenses:
|
145
147
|
- MIT
|
data/lib/typed_form/client.rb
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
module TypedForm
|
2
|
-
class Client
|
3
|
-
attr_reader :api_key
|
4
|
-
|
5
|
-
include HTTParty
|
6
|
-
|
7
|
-
def initialize(api_key:)
|
8
|
-
@api_key = api_key
|
9
|
-
end
|
10
|
-
|
11
|
-
def forms_by_id(form_id:, **query_params)
|
12
|
-
url_params = query_params.map { |k, v| "#{k}=#{v}" }
|
13
|
-
request_url = [form_id, authenticated_slug(url_params)].join("?")
|
14
|
-
get(request_url).body
|
15
|
-
end
|
16
|
-
|
17
|
-
def find_form_by(form_id:, token:, **query_params)
|
18
|
-
forms_by_id(form_id: form_id, token: token, **query_params)
|
19
|
-
end
|
20
|
-
|
21
|
-
private
|
22
|
-
|
23
|
-
def get(slug)
|
24
|
-
self.class.get(base_url + slug)
|
25
|
-
end
|
26
|
-
|
27
|
-
def base_url
|
28
|
-
"https://api.typeform.com/v1/form/"
|
29
|
-
end
|
30
|
-
|
31
|
-
def authenticated_slug(url_params)
|
32
|
-
["key=#{api_key}", url_params].join("&")
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
@@ -1,66 +0,0 @@
|
|
1
|
-
module TypedForm
|
2
|
-
class FormAnswers
|
3
|
-
extend Forwardable
|
4
|
-
|
5
|
-
def_delegators :response, :answers, :metadata, :token
|
6
|
-
def_delegators :metadata, :date_submit
|
7
|
-
|
8
|
-
attr_reader :response, :input_questions, :original_questions
|
9
|
-
|
10
|
-
def self.collate(response:, input_questions:, original_questions:)
|
11
|
-
new(response: response,
|
12
|
-
input_questions: input_questions,
|
13
|
-
original_questions: original_questions).questions
|
14
|
-
end
|
15
|
-
|
16
|
-
def initialize(response:, input_questions:, original_questions:)
|
17
|
-
@response = response
|
18
|
-
@input_questions = input_questions
|
19
|
-
@original_questions = original_questions
|
20
|
-
end
|
21
|
-
|
22
|
-
def questions
|
23
|
-
@_questions ||= build_questions
|
24
|
-
end
|
25
|
-
|
26
|
-
private
|
27
|
-
|
28
|
-
def build_questions
|
29
|
-
input_questions.map do |question|
|
30
|
-
Question.with_response_data(
|
31
|
-
question: question,
|
32
|
-
answer: answers_for(question.ids),
|
33
|
-
text: extrapolated_question_text(question)
|
34
|
-
)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
def answers_for(ids)
|
39
|
-
id_answers = ids.map { |id| find_answer_by_id(id) }.compact
|
40
|
-
return if id_answers.size.zero?
|
41
|
-
id_answers.join(", ")
|
42
|
-
end
|
43
|
-
|
44
|
-
def extrapolated_question_text(question)
|
45
|
-
regex = %r(\{\{answer_(\d+)\}\})
|
46
|
-
id_match = question.original_text.match(regex)
|
47
|
-
return question.original_text unless id_match
|
48
|
-
|
49
|
-
question.original_text.gsub(regex, find_answer_by_field_id(id_match[1]))
|
50
|
-
end
|
51
|
-
|
52
|
-
def find_answer_by_field_id(id)
|
53
|
-
fields = original_questions.select do |question|
|
54
|
-
question.field_id == id.to_i
|
55
|
-
end
|
56
|
-
|
57
|
-
answers_found = fields.map { |field| find_answer_by_id(field.id) }.compact
|
58
|
-
return find_answer_by_id(fields.first.id) if answers_found.size == 1
|
59
|
-
raise ArgumentError, "Cannot find single answer with field ID ##{id}"
|
60
|
-
end
|
61
|
-
|
62
|
-
def find_answer_by_id(id)
|
63
|
-
answers.instance_variable_get("@#{id}")
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
@@ -1,52 +0,0 @@
|
|
1
|
-
module TypedForm
|
2
|
-
class FormResponse
|
3
|
-
attr_reader :parsed_questions, :parsed_response
|
4
|
-
|
5
|
-
def initialize(parsed_questions:, parsed_response:)
|
6
|
-
@parsed_questions = parsed_questions
|
7
|
-
@parsed_response = parsed_response
|
8
|
-
end
|
9
|
-
|
10
|
-
def questions_and_answers
|
11
|
-
FormAnswers.collate(response: parsed_response,
|
12
|
-
input_questions: questions,
|
13
|
-
original_questions: parsed_questions)
|
14
|
-
end
|
15
|
-
|
16
|
-
def questions
|
17
|
-
@_questions ||= build_questions
|
18
|
-
end
|
19
|
-
|
20
|
-
def question_ids
|
21
|
-
questions.flat_map(&:ids)
|
22
|
-
end
|
23
|
-
|
24
|
-
def question_texts
|
25
|
-
questions.map(&:original_text).uniq
|
26
|
-
end
|
27
|
-
|
28
|
-
private
|
29
|
-
|
30
|
-
def question_for_grouped(grouped_questions)
|
31
|
-
question_texts = grouped_questions.map(&:question)
|
32
|
-
return question_texts.first if question_texts.uniq.size == 1
|
33
|
-
raise ArgumentError, "Grouped questions do not have matching text"
|
34
|
-
end
|
35
|
-
|
36
|
-
def answerable_questions
|
37
|
-
parsed_questions
|
38
|
-
.reject { |q| q.id.match /(hidden|legal|statement|group)/ }
|
39
|
-
.group_by(&:field_id)
|
40
|
-
end
|
41
|
-
|
42
|
-
def build_questions
|
43
|
-
answerable_questions.map do |field_id, grouped_questions|
|
44
|
-
Question.new(
|
45
|
-
ids: grouped_questions.map(&:id),
|
46
|
-
field_id: field_id,
|
47
|
-
original_text: question_for_grouped(grouped_questions)
|
48
|
-
).freeze
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
@@ -1,16 +0,0 @@
|
|
1
|
-
module TypedForm
|
2
|
-
class JSONResponseHandler
|
3
|
-
extend Forwardable
|
4
|
-
attr_reader :json
|
5
|
-
|
6
|
-
def_delegators :parsed_json, :questions, :responses
|
7
|
-
|
8
|
-
def initialize(json)
|
9
|
-
@json = json
|
10
|
-
end
|
11
|
-
|
12
|
-
def parsed_json
|
13
|
-
@_parsed_json ||= JSON.parse(json, object_class: Arendelle)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
data/lib/typed_form/question.rb
DELETED
@@ -1,42 +0,0 @@
|
|
1
|
-
module TypedForm
|
2
|
-
class Question
|
3
|
-
attr_reader :ids, :field_id, :original_text
|
4
|
-
attr_accessor :answer, :text
|
5
|
-
|
6
|
-
def initialize(ids:, field_id:, original_text:)
|
7
|
-
@ids = ids
|
8
|
-
@field_id = field_id
|
9
|
-
@original_text = original_text
|
10
|
-
end
|
11
|
-
|
12
|
-
def add_response_data(answer:, text:)
|
13
|
-
@answer = answer
|
14
|
-
@text = text
|
15
|
-
end
|
16
|
-
|
17
|
-
def type
|
18
|
-
@_type ||= determine_type
|
19
|
-
end
|
20
|
-
|
21
|
-
def type_for_grouped(grouped_questions)
|
22
|
-
types = questions.map(&:type)
|
23
|
-
return types.first if types.uniq.size == 1
|
24
|
-
raise ArgumentError, "Grouped questions do not have matching types"
|
25
|
-
end
|
26
|
-
|
27
|
-
def self.with_response_data(question:, text:, answer:)
|
28
|
-
question.dup.tap do |new_question|
|
29
|
-
new_question.answer = answer
|
30
|
-
new_question.text = text
|
31
|
-
end.freeze
|
32
|
-
end
|
33
|
-
|
34
|
-
private
|
35
|
-
|
36
|
-
def determine_type
|
37
|
-
detected_type = ids.map { |id| id.split("_")[0] }.uniq
|
38
|
-
return detected_type.first if detected_type.size == 1
|
39
|
-
raise StandardError, "Cannot detect type of question ids #{ids}"
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|