typeform_data 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 876713987fcf2c378c84995bf1a5c3b0d460755a
4
- data.tar.gz: eaf7a7355ffda4252d318934b02aa806a215544d
3
+ metadata.gz: f66614378d50f785ebb54f8e8739bb30ddbc5f48
4
+ data.tar.gz: 91ef3fe46c067e7f11f5a503e6dc099ce1fe28d7
5
5
  SHA512:
6
- metadata.gz: 3d0b3c91030dd7e26f4f9eb89ce2adf4ba02b7b6e09f885fa22ea1ad12979951cfb88e968d1272cfa9d093cfee8223151b32b8cbf9b469c1251f8435f3295afd
7
- data.tar.gz: e1c74927ec69c7f570d25e5bf8e363faf4df9fde039ca91fee591d7bdebcd3d74ae33f0f6fb62336ee8245c0041e8c0300b348ce99b780392e7c5a615f4cf1ea
6
+ metadata.gz: 02d193b05183e53f3774df96873d6847e353b6f98e3042bd5344f9da37bf5a493e7e6a3c5eb6520646d6d54d8202aff81db045dfb91c0c065630e36c6e12f623
7
+ data.tar.gz: 24f0f010677b83ca46d8b547ec11f66ec6cc95229789fafc01eb142bb9dd95d21e41bfb5beb383125af74e9e63e8f07f72bebf50262b4d8a06de93252518dc36
data/README.md CHANGED
@@ -1,27 +1,74 @@
1
1
  # TypeformData
2
2
 
3
- A Ruby client for Typeform's Data API (https://www.typeform.com/help/data-api/).
3
+ A Ruby client for Typeform's [Data API](https://www.typeform.com/help/data-api/).
4
4
 
5
- This is alpha software and doesn't currently cover all the use cases you'd probably expect. I've just finished implementing response fetching (including de-pagination, so you can fetch _all_ the responses in one method call), but the full data model isn't built out yet.
5
+ **Warning**: this is alpha software, and hasn't been thoroughly vetted in production yet. Use at your own risk :).
6
6
 
7
- Unless you're eager to dive into the code, I'd suggest waiting until next week to check out this gem.
7
+ ## Usage:
8
8
 
9
- Ruby 2.3
9
+ ```
10
+ client = TypeformData::Client.new(api_key: 'YOUR_API_KEY')
11
+ typeforms = client.all_typeforms
12
+
13
+ typeform = typeforms.first
14
+ => #<TypeformData::Typeform
15
+ @config=#<TypeformData::Config @api_key="YOUR_API_KEY">,
16
+ @id="TYPEFORM_ID",
17
+ @name="TYPEFORM_NAME">
18
+ ```
19
+
20
+ ### Fetching responses
21
+
22
+ ```
23
+ all_complete_responses = typeform.responses(completed: true)
24
+ ```
10
25
 
11
- TODO:
12
- - Add more detail, and example method calls.
13
- - Add an explanation: why another gem? What makes this gem different?
26
+ Unless you specify a limit, `TypeformData::Typeform#responses` will not paginate, and will make multiple AJAX requests as needed (the Data API only returns up to 1000 responses at a time) to fetch all the matching responses.
14
27
 
15
- Goals:
16
- - Typeform data (and invidual response data) must work with `Marshal#load` and `Marshal#dump`.
28
+ You can also specify any of the ["Filtering Options"](https://www.typeform.com/help/data-api/) to pass along to the API call:
17
29
 
18
- We have to do a lot of reverse-engineering to get an API that feels reasonably intuitive.
30
+ (*Warning*: the `token` parameter isn't working yet.)
19
31
 
20
- ### Notes on the API
32
+ ```
33
+ some_complete_responses = typeform.responses(limit: 500, offset: 2000, completed: true)
34
+ two_days_of_responses = typeform.responses(from: 1470143917, since: 1470316722)
21
35
 
22
- - ID vs. UID: they aren't used consistently across the docs and actual API responses.
36
+ ```
23
37
 
24
- ## Installation
38
+ ### Questions & answers
39
+
40
+ The response data you get back is represented using classes with defined relationships:
41
+
42
+ ```
43
+ typeform.responses.first.answers.first.typeform == typeform
44
+ => true
45
+
46
+ typeform.fields.map(&:text)
47
+ => ["What is your name?", "What are your favorite colors?", ...]
48
+
49
+ typeform.responses.first.answers.map { |answer| [answer.field_text, answer.value] }`
50
+ => [["What is your name?", "Foo Bar"], ["What are your favorite colors?", ["blue", "orange"]]]
51
+
52
+ ```
53
+
54
+ To access a Typeform's questions, we recommend using `TypeformData::Typeform#fields` instead of `TypeformData::Typeform#questions`. Each `TypeformData::Typeform::Answer` is associated to exactly one `TypeformData::Typeform::Field`, and one or more `TypeformData::Typeform::Question`s.
55
+
56
+ ## Notes on the API
57
+
58
+ So far, we've found Typeform's current Data API to be confusing. In particular, there are a couple design decisions that have been a big source of confusion and friction for us:
59
+
60
+ - Statements (which are sections of text in a Typeform, and can't be answered) and Hidden Fields (data passed into a form, and not provided by the user) are both included under the `'questions'` key in the API's response JSON. From the perspective of a user, we don't think of these as "questions".
61
+ - Each option in a "Picture choice" (and, IIRC "Multiple choice" as well, if multiple choices are allowed) is returned as its own "question" in the response JSON for questions and answers. We feel that it makes more sense to model these as multiple answers to one question, i.e. an Array-valued answer.
62
+
63
+ The main goal of this API wrapper is to encapsulate these implementation details (which we find confusing) and provide a more intuitive API for our application code. This means that our data model must deviating in specific places from the implicit data model expressed in the Data API's JSON responses. We're sacrificing consistency for a more intuitive client API.
64
+
65
+ ## Notes
66
+
67
+ - We haven't tested against any Ruby versions other than 2.3.
68
+ - At the moment, this gem has no runtime dependencies.
69
+ - Under the hood, the object relationships are implemented by storing a reference to a config object containing your API key. This is what allows you to say `answer.typeform.responses` and make an API call originating from a `TypeformData::Typeform::Answer` without having to pass in a reference to a client or your API key (again). To avoid leaking your API key, make sure to clear out the `@config` reference if you serialize any of the objects! We've already done a bit here: if you call `Marshal.dump` on a `TypeformData::ValueClass`, we only serialize attributes (not references, and not the `@config` object).
70
+
71
+ ### Installation
25
72
 
26
73
  Add this line to your application's Gemfile:
27
74
 
@@ -33,25 +80,18 @@ And then execute:
33
80
 
34
81
  $ bundle
35
82
 
36
- Or install it yourself as:
37
-
38
- $ gem install typeform_data
39
-
40
- ## Usage
41
-
42
- TODO: Write usage instructions here
43
-
44
- ## Development
83
+ ### Development
45
84
 
46
85
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
47
86
 
48
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
87
+ ### Releasing a new version
49
88
 
50
- ## Contributing
89
+ Update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
51
90
 
52
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/typeform_data. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
91
+ ### Contributing
53
92
 
93
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/typeform_data. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
54
94
 
55
- ## License
95
+ ### License
56
96
 
57
97
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/lib/typeform_data.rb CHANGED
@@ -31,6 +31,7 @@ require 'typeform_data/client'
31
31
  require 'typeform_data/errors'
32
32
  require 'typeform_data/config'
33
33
  require 'typeform_data/value_class'
34
+ require 'typeform_data/comparable_by_id_and_config'
34
35
 
35
36
  require 'typeform_data/requestor'
36
37
  require 'typeform_data/api_response'
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ module TypeformData
3
+ module ComparableByIdAndConfig
4
+ include Comparable
5
+
6
+ # Override this method to specify a different key.
7
+ def sort_key
8
+ id
9
+ end
10
+
11
+ def ==(other)
12
+ other.sort_key == sort_key && other.config == config
13
+ end
14
+
15
+ def <=>(other)
16
+ other.sort_key <=> sort_key
17
+ end
18
+
19
+ end
20
+ end
@@ -5,10 +5,9 @@ module TypeformData
5
5
 
6
6
  class Typeform
7
7
  include TypeformData::ValueClass
8
+ include TypeformData::ComparableByIdAndConfig
8
9
  readable_attributes :id, :name
9
10
 
10
- # TODO: define comparison methods <=>, etc.
11
-
12
11
  # See https://www.typeform.com/help/data-api/ under "Filtering Options" for the full list of
13
12
  # options.
14
13
  #
@@ -23,8 +22,6 @@ module TypeformData
23
22
  # @param Hash<[String, Symbol], [String, Symbol]> params
24
23
  # @raise TypeformData::ArgumentError
25
24
  def responses(params = {})
26
- # TODO: not sure what the implementation will be here, since responses_request needs to
27
- # handle the awkwardness of returning multiple kinds of data at the same time.
28
25
  response = responses_request(collapse_and_validate_responses_params(params))
29
26
  set_stats(response['stats']['responses'])
30
27
 
@@ -37,28 +34,37 @@ module TypeformData
37
34
  }
38
35
  end
39
36
 
37
+ # Typeform's 'question' concept (as expressed in the API) has the following disadvantages:
38
+ # - Each choice in a multi-select is treated as its own 'question'
39
+ # - Hidden Fields are included as 'questions'
40
+ # - Statements are included as 'questions'
41
+ #
42
+ # In practice, I recommend using TypeformData::Typeform#field instead, as it addresses these
43
+ # issues. Typeform#quesions is here so you have access to the underlying data if you need it.
44
+ #
45
+ # @return [TypeformData::Typeform::Question]
46
+ def questions
47
+ @_questions ||= fetch_questions
48
+ end
49
+
40
50
  def fields
41
51
  @_fields ||= Field.from_questions(config, questions)
42
52
  end
43
53
 
44
- # In general, Typeform's "question" concept is less useful than the field concept. TODO: add
45
- # more notes on this.
46
- def questions
47
- (@_questions ||= fetch_questions).reject(&:hidden_field?)
54
+ def hidden_fields
55
+ questions.select(&:hidden_field?)
48
56
  end
49
57
 
50
- def hidden_fields
51
- (@_questions ||= fetch_questions).select(&:hidden_field?)
58
+ def statements
59
+ questions.select(&:statement?)
52
60
  end
53
61
 
54
62
  def stats
55
63
  @_stats ||= fetch_stats
56
64
  end
57
65
 
58
- def ==(other)
59
- other.id == id && other.config == config
60
- end
61
-
66
+ # This method will make an AJAX request if this Typeform's name hasn't already been set.
67
+ # @return [String]
62
68
  def name
63
69
  return name if name
64
70
  @name ||= client.all_typeforms.find { |typeform| typeform.id == id }.name
@@ -5,23 +5,20 @@ module TypeformData
5
5
  class Answer
6
6
  include TypeformData::ValueClass
7
7
  include TypeformData::Typeform::ById
8
+ include TypeformData::ComparableByIdAndConfig
9
+
10
+ def sort_key
11
+ field_id
12
+ end
8
13
 
9
14
  # field_text may be removed in the future: we may want to normalize our data model. For
10
15
  # 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
16
  #
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
17
+ # The type of 'value' is [String, Fixnum, Array<String>, Array<Fixnum>], since we are
18
+ # combining 'answers' from the API's JSON responses if those answers share the same field.
19
+ readable_attributes :field_id, :value, :field_text, :response_token, :typeform_id
23
20
 
24
- def question_type
21
+ def field_type
25
22
  id.split('_').first
26
23
  end
27
24
 
@@ -30,6 +27,45 @@ module TypeformData
30
27
  # def response
31
28
  # typeform.responses(token: response_token)
32
29
  # end
30
+
31
+ # In the JSON, answer 'ID's are of the form:
32
+ #
33
+ # - "textfield_12316024"
34
+ # - "listimage_12316029_choice_12322262"
35
+ #
36
+ # This list may not be exhaustive-- there may be other ID formats not covererd above-- since
37
+ # this part of the API isn't mentioned in the documentation.
38
+ #
39
+ # For our Answer object, we strip out only the 'listimage_12316029' part, giving Answers the
40
+ # same specificity as Fields.
41
+ #
42
+ # Use this method to create Answers when initializing a Response.
43
+ # @return [Array<Answer>]
44
+ def self.from_response_attrs(config, attrs, fields)
45
+ (attrs[:answers] || attrs['answers']).group_by { |id, _value|
46
+ field_id = id.split('_')[1]
47
+
48
+ unless field_id && field_id.length.positive?
49
+ raise UnexpectedError, 'Falsy field ID for answer(s)'
50
+ end
51
+
52
+ fields.find { |field| field.id.to_s == field_id }.tap { |matched|
53
+ raise UnexpectedError, 'Expected to find a matching field' unless matched
54
+ }
55
+ }.map { |field, ids_and_values|
56
+ values = ids_and_values.map(&:last)
57
+
58
+ Answer.new(
59
+ config,
60
+ field_id: field.id,
61
+ value: values.one? ? values.first : values,
62
+ field_text: field.text,
63
+ response_token: attrs[:token] || attrs['token'],
64
+ typeform_id: attrs[:typeform_id] || attrs['typeform_id'],
65
+ )
66
+ }
67
+ end
68
+
33
69
  end
34
70
 
35
71
  end
@@ -5,22 +5,35 @@ module TypeformData
5
5
  class Field
6
6
  include TypeformData::ValueClass
7
7
  include TypeformData::Typeform::ById
8
- readable_attributes :id, :text, :typeform_id, :question_ids
8
+ include TypeformData::ComparableByIdAndConfig
9
+ readable_attributes :id, :type, :text, :typeform_id, :question_ids
9
10
 
10
11
  # The Data API includes 'statements' as part of a Typeform's "questions", despite the fact
11
12
  # that these statements don't have associated answers.
12
13
  def statement?
13
- id.split('_').first == 'statement'
14
+ type == 'statement'
14
15
  end
15
16
 
16
17
  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'
18
+ input_questions.reject(
19
+ &:hidden_field?
20
+ ).reject(
21
+ &:statement?
22
+ ).group_by(&:field_id).map do |field_id, questions|
23
+ unless 1 == questions.map(&:text).uniq.length && 1 == questions.map(&:type).uniq.length
24
+ # TODO: turn these errors into warnings.
25
+ raise UnexpectedError, 'Expected questions with the same field_id to have the same '\
26
+ 'type and text'
20
27
  end
21
28
 
22
- new(config, id: field_id, text: questions.first.text, question_ids: questions.map(&:id),
23
- typeform_id: questions.first.typeform_id)
29
+ new(
30
+ config,
31
+ id: field_id,
32
+ type: questions.first.type,
33
+ text: questions.first.text,
34
+ question_ids: questions.map(&:id),
35
+ typeform_id: questions.first.typeform_id,
36
+ )
24
37
  end
25
38
  end
26
39
 
@@ -5,19 +5,33 @@ module TypeformData
5
5
  class Question
6
6
  include TypeformData::ValueClass
7
7
  include TypeformData::Typeform::ById
8
+ include TypeformData::ComparableByIdAndConfig
9
+
10
+ # A question ID is of the form:
11
+ # - opinionscale_20576123
12
+ # - listimage_46576029_choice_26422755
13
+ #
14
+ # It looks like the second part of the ID is the field ID, but the Data API includes
15
+ # 'field_id' as a separate property in the response JSON, so I'm not sure if we can treat
16
+ # them as the same.
17
+ #
8
18
  readable_attributes :id, :question, :field_id, :typeform_id
9
19
 
10
20
  # Question#question makes for a bad API. Ideally, use Question#text instead.
11
21
  alias text question
12
22
 
23
+ def type
24
+ id.split('_').first
25
+ end
26
+
13
27
  def hidden_field?
14
- id.split('_').first == 'hidden'
28
+ type == 'hidden'
15
29
  end
16
30
 
17
31
  # The Data API includes 'statements' as part of a Typeform's "questions", despite the fact
18
32
  # that these statements don't have associated answers.
19
33
  def statement?
20
- id.split('_').first == 'statement'
34
+ type == 'statement'
21
35
  end
22
36
  end
23
37
 
@@ -5,8 +5,16 @@ module TypeformData
5
5
  class Response
6
6
  include TypeformData::ValueClass
7
7
  include TypeformData::Typeform::ById
8
+ include TypeformData::ComparableByIdAndConfig
9
+
8
10
  readable_attributes :token, :metadata, :hidden, :typeform_id, :answers, :completed
9
11
 
12
+ alias hidden_fields hidden
13
+
14
+ def sort_key
15
+ token
16
+ end
17
+
10
18
  # It's correct to name this attribute "completed?" and not "complete?" since it's always in
11
19
  # the past tense-- once a potential respondent leaves a Typeform unsubmitted, they can never
12
20
  # go back and complete it.
@@ -14,29 +22,14 @@ module TypeformData
14
22
  @completed == 1
15
23
  end
16
24
 
17
- alias hidden_fields hidden
18
-
19
25
  def date_submitted
20
26
  DateTime.strptime(metadata['date_submit'], '%Y-%m-%d %H:%M:%S')
21
27
  end
22
28
 
23
29
  def initialize(config, attrs, fields)
24
30
  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
-
31
+ mapped_attrs[:answers] = Answer.from_response_attrs(config, attrs, fields)
32
+ mapped_attrs.delete('answers')
40
33
  super(config, mapped_attrs)
41
34
  end
42
35
 
@@ -45,10 +38,6 @@ module TypeformData
45
38
  answers.each { |answer| answer.config = config }
46
39
  end
47
40
 
48
- def ==(other)
49
- other.token == token && other.config == config
50
- end
51
-
52
41
  end
53
42
 
54
43
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module TypeformData
3
- VERSION = '0.0.2'
3
+ VERSION = '0.0.3'
4
4
  end
@@ -9,15 +9,13 @@ Gem::Specification.new do |spec|
9
9
  spec.name = 'typeform_data'
10
10
  spec.version = TypeformData::VERSION
11
11
  spec.authors = ['Max Wallace']
12
- spec.email = ['maxfield.wallace@gmail.com']
12
+ spec.email = ['engineering@shearwaterintl.com']
13
13
 
14
- spec.summary = 'An opinionated client for the Typeform.com Data API'
15
- spec.description = 'typeform_data is a minimal, opinionated client for the Typeform.com Data '\
16
- 'API (see https://www.typeform.com/help/data-api/). The goal of this '\
17
- 'project is to create a maintainable, extensible client that provides a '\
18
- "more natural object-oriented interface to Typeform.com's Data API."
14
+ spec.summary = 'An opinionated, OO client for the Typeform.com Data API'
15
+ spec.description = 'typeform_data is a minimal, opinionated, OO client for the Typeform.com '\
16
+ 'Data API with no runtime dependencies.'
19
17
 
20
- spec.homepage = 'https://github.com/shearwater-intl/typeform_data'
18
+ spec.homepage = 'https://github.com/shearwaterintl/typeform_data'
21
19
  spec.license = 'MIT'
22
20
 
23
21
  spec.files = `git ls-files -z`.split("\x0").reject { |f|
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.2
4
+ version: 0.0.3
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-08-02 00:00:00.000000000 Z
11
+ date: 2016-08-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -136,12 +136,10 @@ dependencies:
136
136
  - - "~>"
137
137
  - !ruby/object:Gem::Version
138
138
  version: '0.39'
139
- description: typeform_data is a minimal, opinionated client for the Typeform.com Data
140
- API (see https://www.typeform.com/help/data-api/). The goal of this project is to
141
- create a maintainable, extensible client that provides a more natural object-oriented
142
- interface to Typeform.com's Data API.
139
+ description: typeform_data is a minimal, opinionated, OO client for the Typeform.com
140
+ Data API with no runtime dependencies.
143
141
  email:
144
- - maxfield.wallace@gmail.com
142
+ - engineering@shearwaterintl.com
145
143
  executables: []
146
144
  extensions: []
147
145
  extra_rdoc_files: []
@@ -160,6 +158,7 @@ files:
160
158
  - lib/typeform_data.rb
161
159
  - lib/typeform_data/api_response.rb
162
160
  - lib/typeform_data/client.rb
161
+ - lib/typeform_data/comparable_by_id_and_config.rb
163
162
  - lib/typeform_data/config.rb
164
163
  - lib/typeform_data/errors.rb
165
164
  - lib/typeform_data/requestor.rb
@@ -173,7 +172,7 @@ files:
173
172
  - lib/typeform_data/value_class.rb
174
173
  - lib/typeform_data/version.rb
175
174
  - typeform_data.gemspec
176
- homepage: https://github.com/shearwater-intl/typeform_data
175
+ homepage: https://github.com/shearwaterintl/typeform_data
177
176
  licenses:
178
177
  - MIT
179
178
  metadata: {}
@@ -196,5 +195,5 @@ rubyforge_project:
196
195
  rubygems_version: 2.5.1
197
196
  signing_key:
198
197
  specification_version: 4
199
- summary: An opinionated client for the Typeform.com Data API
198
+ summary: An opinionated, OO client for the Typeform.com Data API
200
199
  test_files: []