typed_form 0.0.4 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|