typeform_data 0.0.1 → 0.0.2

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: 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