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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 08af965b58a09c76f9fe8f5937c431cb73a4b09d
4
- data.tar.gz: a9764f0ba29abe0b8ea53a7b82ebb037f1766c79
3
+ metadata.gz: 876713987fcf2c378c84995bf1a5c3b0d460755a
4
+ data.tar.gz: eaf7a7355ffda4252d318934b02aa806a215544d
5
5
  SHA512:
6
- metadata.gz: b5f151253223bff48b951c368ed3822d1df15292ddf3c400ab0104ea3dd1286767497e6ec682681781abb561103a0ab642d60d5e850c9af0093fabfe5cdbc067
7
- data.tar.gz: a8025b7f9b7557086ebf127cbe4b6756e16af1223c96bbcfd4f28c795de1131da494d697a47e760c016746b88c2a16f9d2e341c8584f481340dd5f578139721f
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'
@@ -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
- @_requestor = ::TypeformData::Requestor.new(api_key: api_key)
10
+ @config = TypeformData::Config.new(api_key: api_key)
9
11
  end
10
12
 
11
- def all_typeforms
12
- get('forms').parsed_json.map do |form_hash|
13
- ::TypeformData::Typeform.new(self, form_hash)
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
- @_requestor.get(endpoint, params)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- module TypeformDataClient
2
+ module TypeformData
3
3
  class Error < StandardError; end
4
4
  class InvalidApiKey < Error; end
5
5
  class ConnectionRefused < Error; end
@@ -1,29 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TypeformData
4
- class Requestor
4
+ module Requestor
5
5
 
6
- def initialize(api_key:)
7
- @config = ::TypeformData::Requestor::Config.new(api_key: api_key)
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 get(endpoint, params = nil)
11
- request(Net::HTTP::Get, request_path(endpoint), params)
10
+ def self.request_path(config, endpoint)
11
+ "/v#{config.api_version}/#{endpoint}"
12
12
  end
13
13
 
14
- private
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] = @config.api_key
20
+ params[:key] = config.api_key
25
21
 
26
- response = Net::HTTP.new(@config.host, @config.port).tap { |http|
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 TypeformDataClient::InvalidEndpointOrMissingResource, path
36
+ raise TypeformData::InvalidEndpointOrMissingResource, path
41
37
  when Net::HTTPForbidden then
42
- raise TypeformDataClient::InvalidApiKey, "Invalid api key: #{@config.api_key}"
38
+ raise TypeformData::InvalidApiKey, "Invalid api key: #{config.api_key}"
43
39
  when Net::HTTPBadRequest then
44
- raise TypeformDataClient::BadRequest, 'There was an error processing your request: '\
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 TypeformDataClient::UnexpectedError, "A #{response.code} error has occurred: "\
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 TypeformDataClient::ConnectionRefused, 'The connection was refused'
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
- str_id = ''
22
-
23
- begin
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
- PERMITTED_KEYS = {
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 { |hash|
64
- Response.from_hash(self, hash)
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).parsed_json['questions'] || []
78
- questions.map { |hash| Question.from_hash(self, hash) }
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
- stats_hash = responses_request(limit: 1)['stats']['responses']
83
- Stats.from_stats_hash(stats_hash)
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
- # Check if we've got everything.
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, @client.get('form/' + id, params))]
97
+ requests = [ResponsesRequest.new(params, client.get('form/' + id, params))]
105
98
 
106
99
  loop do
107
- if requests.last.last_request?
108
- break
109
- else
110
- next_params = requests.last.params.dup
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.from_hash(self, hash) }
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(stats_hash)
132
- @_stats = Stats.from_stats_hash(stats_hash)
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[key]
142
- params.delete(key)
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,13 @@
1
+ # frozen_string_literal: true
2
+ module TypeformData
3
+ class Typeform
4
+
5
+ module ById
6
+ def typeform
7
+ raise UnexpectedError, 'Expected a defined typeform_id' unless typeform_id
8
+ client.typeform(typeform_id)
9
+ end
10
+ end
11
+
12
+ end
13
+ 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
- attr_reader :id
7
- attr_reader :question
8
- attr_reader :field_id
6
+ include TypeformData::ValueClass
7
+ include TypeformData::Typeform::ById
8
+ readable_attributes :id, :question, :field_id, :typeform_id
9
9
 
10
- def initialize(typeform, id:, question:, field_id:)
11
- @typeform = typeform
12
- @id = id
13
- @question = question
14
- @field_id = field_id
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
- def self.from_hash(typeform, hash)
18
- new(
19
- typeform,
20
- id: hash['id'],
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
- attr_reader :typeform
7
- attr_reader :token
8
- attr_reader :metadata
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 initialize(typeform, token:, metadata:, hidden:, completed:)
18
- @typeform = typeform
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 self.from_hash(typeform, hash)
26
- new(
27
- typeform,
28
- token: hash['token'],
29
- metadata: hash['metadata'],
30
- hidden: hash['hidden'],
31
- completed: hash['completed']
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
- attr_reader :showing
7
- attr_reader :total
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
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module TypeformData
3
- VERSION = '0.0.1'
3
+ VERSION = '0.0.2'
4
4
  end
@@ -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.1
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-07-29 00:00:00.000000000 Z
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