medic 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +210 -10
  3. data/lib/medic.rb +1 -3
  4. data/lib/medic/anchor.rb +32 -0
  5. data/lib/medic/anchored_object_query.rb +18 -0
  6. data/lib/medic/correlation_query.rb +22 -0
  7. data/lib/medic/finders.rb +122 -0
  8. data/lib/medic/hk_constants.rb +76 -0
  9. data/lib/medic/interval.rb +29 -0
  10. data/lib/medic/medic.rb +68 -1
  11. data/lib/medic/observer_query.rb +15 -0
  12. data/lib/medic/predicate.rb +39 -0
  13. data/lib/medic/query_options.rb +23 -0
  14. data/lib/medic/sample_query.rb +22 -0
  15. data/lib/medic/sort.rb +24 -0
  16. data/lib/medic/source_query.rb +15 -0
  17. data/lib/medic/statistics_collection_query.rb +39 -0
  18. data/lib/medic/statistics_options.rb +31 -0
  19. data/lib/medic/statistics_query.rb +17 -0
  20. data/lib/medic/store.rb +117 -0
  21. data/lib/medic/types.rb +100 -0
  22. data/lib/medic/version.rb +1 -1
  23. data/spec/medic/anchor.rb +16 -0
  24. data/spec/medic/anchored_object_query_spec.rb +12 -0
  25. data/spec/medic/correlation_query_spec.rb +16 -0
  26. data/spec/medic/hk_constants_spec.rb +67 -0
  27. data/spec/medic/interval_spec.rb +16 -0
  28. data/spec/medic/medic_spec.rb +153 -0
  29. data/spec/medic/observer_query_spec.rb +12 -0
  30. data/spec/medic/predicate_spec.rb +27 -0
  31. data/spec/medic/query_options_spec.rb +24 -0
  32. data/spec/medic/sample_query_spec.rb +12 -0
  33. data/spec/medic/sort_spec.rb +26 -0
  34. data/spec/medic/source_query_spec.rb +12 -0
  35. data/spec/medic/statistics_collection_query_spec.rb +27 -0
  36. data/spec/medic/statistics_options_spec.rb +32 -0
  37. data/spec/medic/statistics_query_spec.rb +12 -0
  38. data/spec/medic/store_spec.rb +159 -0
  39. data/spec/medic/types_spec.rb +95 -0
  40. metadata +72 -9
  41. data/spec/main_spec.rb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: de19fe133b21f0149428e905fed5cae6920e29d1
4
- data.tar.gz: 7481827c1465cc8695b4edcb18e5e89a89244d1a
3
+ metadata.gz: bf3a75beba5a3f3be3df75476d6d18d188752bac
4
+ data.tar.gz: 01bb258ba550c735090e7d04df792972562f4f91
5
5
  SHA512:
6
- metadata.gz: 2ba5162cf3304ba27edbf19e9b7c99d09035d295c4f1815f66bced596d30021717f9ec6c0dd4865b8b9cc375edd094407221cbf02709b3c5876cfda0f6069680
7
- data.tar.gz: 625255212fed825cae342212d3d944c61c2317c459b3ccc5507545feccb7fc141f3176b03d7eae8fc367b0e87cb9c52b48cb2ad9e7051a28365ee8f6e81fbd20
6
+ metadata.gz: 02ff5adbb1f076e1e95fee777d4d5289e0bd1eb73808f32d3ee90a6b2a45aafc27b13e323ea96885e55294e4356d2eb783cbd02e7b271287a7dc1afa07df7fa1
7
+ data.tar.gz: 780718864993970ea3889ac42df1627066152d3862aab9be21f7f5eded7b146195874fff3c1a68497dc46b14482aa5348f089c70eac31e9572250511ee34ca8e
data/README.md CHANGED
@@ -1,24 +1,224 @@
1
- # medic
1
+ # Medic [![Gem Version](https://badge.fury.io/rb/medic.svg)](http://badge.fury.io/rb/medic) [![Build Status](https://travis-ci.org/ryanlntn/medic.svg)](https://travis-ci.org/ryanlntn/medic) [![Code Climate](https://codeclimate.com/github/ryanlntn/medic/badges/gpa.svg)](https://codeclimate.com/github/ryanlntn/medic) [![Dependency Status](https://gemnasium.com/ryanlntn/medic.svg)](https://gemnasium.com/ryanlntn/medic)
2
2
 
3
- TODO: Write a gem description
3
+ Is HealthKit's verbose and convoluted API driving you to autolobotomization? Quick! You need a medic!
4
4
 
5
5
  ## Installation
6
6
 
7
- Add this line to your application's Gemfile:
7
+ 1. Add this line to your application's Gemfile:
8
8
 
9
- gem 'medic'
9
+ ```ruby
10
+ gem 'medic'
11
+ ```
10
12
 
11
- And then execute:
13
+ 1. Add the following lines to your Rakefile:
12
14
 
13
- $ bundle
15
+ ```ruby
16
+ app.entitlements['com.apple.developer.healthkit'] = true
17
+ app.frameworks += ['HealthKit']
18
+ ```
14
19
 
15
- Or install it yourself as:
16
-
17
- $ gem install medic
20
+ 1. Run `bundle` and `rake`.
18
21
 
19
22
  ## Usage
20
23
 
21
- TODO: Write usage instructions here
24
+ ### Authorization
25
+
26
+ To request authorization to read or share (i.e. write) a data type implement the following in `viewDidAppear` of your `UIViewController`:
27
+
28
+ ```ruby
29
+ if Medic.available?
30
+ types = { share: :step_count, read: [:step_count, :date_of_birth] }
31
+
32
+ Medic.authorize types do |success, error|
33
+ NSLog "Success!" if success
34
+ end
35
+ end
36
+ ```
37
+
38
+ This will open the permissions modal. `success` will be `false` if the user canceled the prompt without selecting permissions; `true` otherwise.
39
+
40
+ You can subsequently check if you're authorized to share a data type:
41
+
42
+ ```ruby
43
+ Medic.authorized?(:step_count)
44
+ ```
45
+
46
+ Note: For privacy reasons Apple does not allow you to check if you're authorized to read data types.
47
+
48
+ ### Sharing Samples
49
+
50
+ Coming soon...
51
+
52
+ ### Reading Data
53
+
54
+ HealthKit provides a number of methods for accessing its data, mostly in the form of query objects with verbose initializers that return more `HKObject`s with repetitive method names. For example, if I wanted to get the total number of steps taken per day for the last week I could use a `HKStatisticsCollectionQuery` like so:
55
+
56
+ ```ruby
57
+ @store = HKHealthStore.new
58
+
59
+ today = NSDate.date
60
+ one_week_ago = NSCalendar.currentCalendar.dateByAddingComponents(NSDateComponents.new.setDay(-7), toDate: today, options: 0)
61
+
62
+ query = HKStatisticsCollectionQuery.initWithQuantityType(
63
+ HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount),
64
+ quantitySamplePredicate: nil,
65
+ options: HKStatisticsOptionCumulativeSum,
66
+ anchorDate: one_week_ago,
67
+ intervalComponents: (NSDateComponents.new.day = 1)
68
+ )
69
+
70
+ query.initialResultsHandler = ->(query, results, error){
71
+ results.enumerateStatisticsFromDate(one_week_ago, toDate: today, withBlock: ->(result, stop){
72
+ if quantity = result.sumQuantity
73
+ NSLog quantity.doubleValueForUnit(HKUnit.countUnit).to_s
74
+ end
75
+ })
76
+ }
77
+
78
+ @store.executeQuery(query)
79
+ ```
80
+
81
+ This doesn't make for the most readable code. As a Ruby developer you might find this downright distasteful. Let's check out the Medic equivalent:
82
+
83
+ ```ruby
84
+ options = { options: :sum, anchor: :one_week_ago, interval: :day}
85
+
86
+ Medic.find_statistics_collection :step_count, options do |statistics|
87
+ statistics.each do |stats|
88
+ NSLog stats[:sum].to_s
89
+ end
90
+ end
91
+ ```
92
+
93
+ Now that's more like it! Instead of constructing `HKObjectType` by hand we can now just pass in a symbol. We also don't have to work directly with `HKStatisticsCollection` anymore. The result is parsed into an array of hashes with reasonable values for us.
94
+
95
+ #### Finders
96
+
97
+ Medic provides a finder for each class of `HKQuery`:
98
+
99
+ ##### find_anchored
100
+
101
+ Provide an easy way to search for new data in the HealthKit store.
102
+
103
+ ```ruby
104
+ @anchor = nil
105
+
106
+ Medic.find_anchored :step_count, anchor: @anchor do |results, new_anchor|
107
+ @anchor = new_anchor
108
+ results.each do |sample|
109
+ NSLog sample.to_s
110
+ end
111
+ end
112
+ ```
113
+
114
+ ##### find_correlation
115
+
116
+ Search for correlations in the HealthKit store.
117
+
118
+ ```ruby
119
+ high_cal = HKQuantity.quantityWithUnit(HKUnit.kilocalorieUnit, doubleValue: 800.0)
120
+ greater_than_high_cal = HKQuery.predicateForQuantitySamplesWithOperatorType(NSGreaterThanOrEqualToPredicateOperatorType, quantity: high_cal)
121
+
122
+ sample_predicates = { dietary_energy_consumed: greater_than_high_cal }
123
+
124
+ Medic.find_correlations :food, sample_predicates: sample_predicates do |correlations|
125
+ correlations.each do |correlation|
126
+ NSLog correlation.to_s
127
+ end
128
+ end
129
+ ```
130
+
131
+ ##### observe
132
+
133
+ Set up a long-running task on a background queue.
134
+
135
+ ```ruby
136
+ Medic.observe :step_count do |completion, error|
137
+ Medic.find_sources :step_count do |sources|
138
+ sources.each do |source|
139
+ NSLog source
140
+ end
141
+ end
142
+ end
143
+ ```
144
+
145
+ ##### find_samples
146
+
147
+ Search for sample data in the HealthKit store.
148
+
149
+ ```ruby
150
+ Medic.find_samples :blood_pressure, sort: :start_date, limit: 7 do |samples|
151
+ samples.each do |sample|
152
+ NSLog sample.to_s
153
+ end
154
+ end
155
+ ```
156
+
157
+ ##### find_sources
158
+
159
+ Search for the sources (apps and devices) that have saved data to the HealthKit store.
160
+
161
+ ```ruby
162
+ Medic.find_sources :step_count do |sources|
163
+ sources.each do |source|
164
+ NSLog source
165
+ end
166
+ end
167
+ ```
168
+
169
+ ##### find_statistics
170
+
171
+ Perform statistical calculations over the set of matching quantity samples.
172
+
173
+ ```ruby
174
+ Medic.find_statistics :step_count, options: :sum do |statistics|
175
+ NSLog statistics.to_s
176
+ end
177
+ ```
178
+
179
+ ##### find_statistics_collection
180
+
181
+ Perform multiple statistics queries over a series of fixed-length time intervals.
182
+
183
+ ```ruby
184
+ options = { options: :sum, anchor: :one_week_ago, interval: :day}
185
+
186
+ Medic.find_statistics_collection :step_count, options do |statistics|
187
+ statistics.each do |stats|
188
+ NSLog stats[:sum].to_s
189
+ end
190
+ end
191
+ ```
192
+
193
+ #### Characteristic Data
194
+
195
+ Characteristic data like biological sex or blood type have their own methods:
196
+
197
+ ```ruby
198
+ Medic.biological_sex # => :male
199
+ Medic.date_of_birth # => 1987-11-07 00:00:00 -0800
200
+ Medic.blood_type # => :o_negative
201
+ ```
202
+
203
+ #### Queries
204
+
205
+ If for some reason you need to access the `HKSample` objects directly you can use Medic's Query objects:
206
+
207
+ ```ruby
208
+ query_params = { type: :dietary_protein, sort_desc: :start_date, limit: 7 }
209
+
210
+ query = Medic::SampleQuery.new query_params do |query, results, error|
211
+ if results
212
+ results.each do |sample|
213
+ NSLog "#{sample.startDate} - #{sample.quantity.doubleValueForUnit(HKUnit.gramUnit)}"
214
+ end
215
+ else
216
+ NSLog "no results"
217
+ end
218
+ end
219
+
220
+ Medic.execute(query)
221
+ ```
22
222
 
23
223
  ## Contributing
24
224
 
data/lib/medic.rb CHANGED
@@ -1,10 +1,8 @@
1
- # encoding: utf-8
2
-
3
1
  unless defined?(Motion::Project::Config)
4
2
  raise "This file must be required within a RubyMotion project Rakefile."
5
3
  end
6
4
 
7
5
  lib_dir_path = File.dirname(File.expand_path(__FILE__))
8
6
  Motion::Project::App.setup do |app|
9
- app.files.unshift(Dir.glob(File.join(lib_dir_path, "project/**/*.rb")))
7
+ app.files.unshift(Dir.glob(File.join(lib_dir_path, "medic/**/*.rb")))
10
8
  end
@@ -0,0 +1,32 @@
1
+ module Medic
2
+ module Anchor
3
+
4
+ NUMBER_WORDS = {
5
+ 'zero' => 0, 'one' => 1, 'two' => 2, 'three' => 3, 'four' => 4, 'five' => 5,
6
+ 'six' => 6, 'seven' => 7, 'eight' => 8, 'nine' => 9, 'ten' => 10, 'eleven' => 11,
7
+ 'twelve' => 12, 'thirteen' => 13, 'fourteen' => 14, 'fifteen' => 15, 'sixteen' => 16,
8
+ 'seventeen' => 17, 'eighteen' => 18, 'nineteen' => 19, 'twenty' => 20, 'thirty' => 30,
9
+ 'fourty' => 40, 'fifty' => 50, 'sixty' => 60, 'seventy' => 70, 'eighty' => 80,
10
+ 'ninety' => 90, 'hundred' => 100
11
+ }
12
+
13
+ def anchor_for_symbol(sym)
14
+ return unless sym
15
+ return sym if sym.to_s == '0'
16
+ return sym if sym.is_a? NSDate
17
+ parts = sym.to_s.gsub('_', ' ').split.reject{ |part| part == 'ago' }
18
+ component = parts.pop.chomp('s')
19
+ n = parts.map{|p| NUMBER_WORDS[p] || p.to_i}.reduce do |sum, p|
20
+ if p == 100 && sum > 0
21
+ sum * p
22
+ else
23
+ sum + p
24
+ end
25
+ end
26
+ n ||= 1
27
+ date_comp = NSDateComponents.new.send("#{component}=", -n)
28
+ NSCalendar.currentCalendar.dateByAddingComponents(date_comp, toDate: NSDate.date, options: 0)
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,18 @@
1
+ module Medic
2
+ class AnchoredObjectQuery < HKAnchoredObjectQuery
3
+
4
+ include Medic::Types
5
+ include Medic::Predicate
6
+ include Medic::Anchor
7
+
8
+ def initialize(args={}, block=Proc.new)
9
+ self.initWithType(object_type(args[:type]),
10
+ predicate: predicate(args),
11
+ anchor: anchor_for_symbol(args[:anchor_date] || args[:anchor] || args[:date] || HKAnchoredObjectQueryNoAnchor),
12
+ limit: args[:limit] || HKObjectQueryNoLimit,
13
+ completionHandler: block
14
+ )
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,22 @@
1
+ module Medic
2
+ class CorrelationQuery < HKCorrelationQuery
3
+
4
+ include Medic::Types
5
+ include Medic::Predicate
6
+
7
+ def initialize(args={}, block=Proc.new)
8
+ self.initWithType(object_type(args[:type]),
9
+ predicate: predicate(args),
10
+ samplePredicates: sample_predicates(args[:sample_predicates]),
11
+ completion: block
12
+ )
13
+ end
14
+
15
+ private
16
+
17
+ def sample_predicates(predicates)
18
+ Hash[ predicates.map{ |type, pred| [object_type(type), predicate(pred)] } ]
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,122 @@
1
+ module Medic
2
+ module Finders
3
+
4
+ def observe(type, options={}, block=Proc.new)
5
+ query_params = options.merge(type: type)
6
+ query = Medic::ObserverQuery.new query_params do |query, completion, error|
7
+ block.call(completion, error)
8
+ end
9
+ Medic.execute(query)
10
+ end
11
+
12
+ def find_sources(type, options={}, block=Proc.new)
13
+ query_params = options.merge(type: type)
14
+ query = Medic::SourceQuery.new query_params do |query, results, error|
15
+ sources = results ? results.allObjects.map{ |source| source.name.to_s } : []
16
+ block.call(sources)
17
+ end
18
+ Medic.execute(query)
19
+ end
20
+
21
+ def find_samples(type, options={}, block=Proc.new)
22
+ query_params = options.merge(type: type)
23
+ query = Medic::SampleQuery.new query_params do |query, results, error|
24
+ block.call(samples_to_hashes(Array(results)))
25
+ end
26
+ Medic.execute(query)
27
+ end
28
+
29
+ def find_correlations(type, options={}, block=Proc.new)
30
+ query_params = options.merge(type: type)
31
+ query = Medic::CorrelationQuery.new query_params do |query, correlations, error|
32
+ block.call(samples_to_hashes(Array(correlations)))
33
+ end
34
+ Medic.execute(query)
35
+ end
36
+
37
+ def find_anchored(type, options={}, block=Proc.new)
38
+ query_params = options.merge(type: type)
39
+ query = Medic::AnchoredObjectQuery.new query_params do |query, results, new_anchor, error|
40
+ block.call(samples_to_hashes(Array(results)), new_anchor)
41
+ end
42
+ Medic.execute(query)
43
+ end
44
+
45
+ def find_statistics(type, options={}, block=Proc.new)
46
+ query_params = options.merge(type: type)
47
+ query = Medic::StatisticsQuery.new query_params do |query, statistics, error|
48
+ block.call(statistics_to_hash(statistics)) if statistics
49
+ end
50
+ Medic.execute(query)
51
+ end
52
+
53
+ def find_statistics_collection(type, options={}, block=Proc.new)
54
+ query_params = options.merge(type: type)
55
+ query = Medic::StatisticsCollectionQuery.new query_params do |query, collection, error|
56
+ formatted_stats = []
57
+ collection.enumerateStatisticsFromDate(collection.anchorDate, toDate: NSDate.date, withBlock: ->(result, stop){
58
+ formatted_stats << statistics_to_hash(result)
59
+ })
60
+ block.call(formatted_stats)
61
+ end
62
+ Medic.execute(query)
63
+ end
64
+
65
+ private
66
+
67
+ def samples_to_hashes(samples)
68
+ samples.map do |sample|
69
+ h = {}
70
+ h[:uuid] = sample.UUID.UUIDString
71
+ h[:metadata] = sample.metadata
72
+ h[:source] = sample.source.name
73
+ h[:start_date] = sample.startDate
74
+ h[:end_date] = sample.endDate
75
+ h[:sample_type] = Medic::Types::TYPE_IDENTIFIERS.index(sample.sampleType.identifier)
76
+
77
+ if sample.respond_to?(:categoryType) && sample.respond_to?(:value)
78
+ h[:category_type] = Medic::Types::TYPE_IDENTIFIERS.index(sample.categoryType.identifier)
79
+ h[:value] = [:in_bed, :asleep][sample.value] # SleepAnalysis is the only category type at the moment
80
+ end
81
+
82
+ if sample.respond_to?(:correlationType) && sample.respond_to?(:objects)
83
+ h[:correlation_type] = Medic::Types::TYPE_IDENTIFIERS.index(sample.correlationType.identifier)
84
+ h[:objects] = samples_to_hashes(Array(sample.objects.allObjects))
85
+ end
86
+
87
+ if sample.respond_to?(:quantity) && sample.respond_to?(:quantityType)
88
+ h[:quantity] = sample.quantity.doubleValueForUnit(sample.quantityType.canonicalUnit)
89
+ h[:quantity_type] = Medic::Types::TYPE_IDENTIFIERS.index(sample.quantityType.identifier)
90
+ h[:canonical_unit] = sample.quantityType.canonicalUnit.unitString
91
+ end
92
+
93
+ h[:duration] = sample.duration if sample.respond_to?(:duration)
94
+ h[:total_distance] = sample.totalDistance if sample.respond_to?(:totalDistance)
95
+ h[:total_energy_burned] = sample.totalEnergyBurned if sample.respond_to?(:totalEnergyBurned)
96
+ h[:workout_activity_type] = sample.workoutActivityType if sample.respond_to?(:workoutActivityType)
97
+ h[:workout_events] = sample.workoutEvents if sample.respond_to?(:workoutEvents)
98
+ h
99
+ end
100
+ end
101
+
102
+ def statistics_to_hash(stats)
103
+ h = {}
104
+ h[:start_date] = stats.startDate
105
+ h[:end_date] = stats.endDate
106
+ h[:sources] = stats.sources.map(&:name) if stats.sources
107
+ h[:quantity_type] = Medic::Types::TYPE_IDENTIFIERS.index(stats.quantityType.identifier)
108
+ h[:canonical_unit] = stats.quantityType.canonicalUnit.unitString
109
+ h[:data_count] = stats.dataCount
110
+ h[:average] = stats.averageQuantity.doubleValueForUnit(stats.quantityType.canonicalUnit) if stats.averageQuantity
111
+ h[:minimum] = stats.minimumQuantity.doubleValueForUnit(stats.quantityType.canonicalUnit) if stats.minimumQuantity
112
+ h[:maximum] = stats.maximumQuantity.doubleValueForUnit(stats.quantityType.canonicalUnit) if stats.maximumQuantity
113
+ h[:sum] = stats.sumQuantity.doubleValueForUnit(stats.quantityType.canonicalUnit) if stats.sumQuantity
114
+ h[:average_by_source] = stats.averageQuantityBySource.doubleValueForUnit(stats.quantityType.canonicalUnit) if stats.averageQuantityBySource
115
+ h[:minimum_by_source] = stats.minimumQuantityBySource.doubleValueForUnit(stats.quantityType.canonicalUnit) if stats.minimumQuantityBySource
116
+ h[:maximum_by_source] = stats.maximumQuantityBySource.doubleValueForUnit(stats.quantityType.canonicalUnit) if stats.maximumQuantityBySource
117
+ h[:sum_by_source] = stats.sumQuantityBySource.doubleValueForUnit(stats.quantityType.canonicalUnit) if stats.sumQuantityBySource
118
+ h
119
+ end
120
+
121
+ end
122
+ end