medic 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.
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