neuron-client 0.2.6 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. data/README.md +11 -8
  2. data/lib/neuron-client/api.rb +5 -0
  3. data/lib/neuron-client/model/ad.rb +142 -0
  4. data/lib/neuron-client/model/ad_calculations.rb +325 -0
  5. data/lib/neuron-client/model/ad_zone.rb +45 -0
  6. data/lib/neuron-client/model/base.rb +233 -18
  7. data/lib/neuron-client/model/blocked_referer.rb +20 -0
  8. data/lib/neuron-client/model/blocked_user_agent.rb +21 -0
  9. data/lib/neuron-client/model/geo_target.rb +36 -0
  10. data/lib/neuron-client/model/pixel.rb +22 -0
  11. data/lib/neuron-client/model/report.rb +35 -0
  12. data/lib/neuron-client/model/s3_file.rb +30 -0
  13. data/lib/neuron-client/model/zone.rb +64 -0
  14. data/lib/neuron-client/model/zone_calculations.rb +37 -0
  15. data/lib/neuron-client/schema/ad.rb +406 -0
  16. data/lib/neuron-client/schema/ad_zone.rb +49 -0
  17. data/lib/neuron-client/schema/blocked_referer.rb +52 -0
  18. data/lib/neuron-client/schema/blocked_user_agent.rb +64 -0
  19. data/lib/neuron-client/schema/common.rb +220 -0
  20. data/lib/neuron-client/schema/event.rb +17 -0
  21. data/lib/neuron-client/schema/geo_target.rb +33 -0
  22. data/lib/neuron-client/schema/pixel.rb +39 -0
  23. data/lib/neuron-client/schema/report.rb +59 -0
  24. data/lib/neuron-client/schema/s3_file.rb +87 -0
  25. data/lib/neuron-client/schema/zone.rb +214 -0
  26. data/lib/neuron-client/version.rb +1 -1
  27. data/lib/neuron-client.rb +24 -59
  28. data/neuron-client.gemspec +3 -0
  29. data/spec/lib/admin_connection_spec.rb +234 -0
  30. data/spec/lib/api_spec.rb +41 -63
  31. data/spec/lib/model/ad_calculations_spec.rb +1146 -0
  32. data/spec/lib/model/ad_spec.rb +253 -0
  33. data/spec/lib/model/ad_zone_spec.rb +15 -0
  34. data/spec/lib/model/base_spec.rb +5 -83
  35. data/spec/lib/model/blocked_referer_spec.rb +36 -0
  36. data/spec/lib/model/blocked_user_agent_spec.rb +36 -0
  37. data/spec/lib/model/geo_target_spec.rb +28 -0
  38. data/spec/lib/model/pixel_spec.rb +36 -0
  39. data/spec/lib/model/report_spec.rb +17 -0
  40. data/spec/lib/{s3_file_spec.rb → model/s3_file_spec.rb} +6 -5
  41. data/spec/lib/model/zone_calculations_spec.rb +49 -0
  42. data/spec/lib/model/zone_spec.rb +155 -0
  43. data/spec/lib/schema/ad_spec.rb +515 -0
  44. data/spec/lib/schema/ad_zone_spec.rb +149 -0
  45. data/spec/lib/schema/blocked_referer_spec.rb +136 -0
  46. data/spec/lib/schema/blocked_user_agent_spec.rb +147 -0
  47. data/spec/lib/schema/geo_target_spec.rb +92 -0
  48. data/spec/lib/schema/pixel_spec.rb +125 -0
  49. data/spec/lib/schema/report_spec.rb +129 -0
  50. data/spec/lib/schema/s3_file_spec.rb +164 -0
  51. data/spec/lib/schema/zone_spec.rb +243 -0
  52. data/spec/spec_helper.rb +2 -1
  53. metadata +141 -121
  54. data/lib/neuron-client/model/admin/ad.rb +0 -22
  55. data/lib/neuron-client/model/admin/ad_zone.rb +0 -15
  56. data/lib/neuron-client/model/admin/base.rb +0 -91
  57. data/lib/neuron-client/model/admin/blocked_referer.rb +0 -12
  58. data/lib/neuron-client/model/admin/blocked_user_agent.rb +0 -12
  59. data/lib/neuron-client/model/admin/geo_target.rb +0 -16
  60. data/lib/neuron-client/model/admin/pixel.rb +0 -12
  61. data/lib/neuron-client/model/admin/report.rb +0 -15
  62. data/lib/neuron-client/model/admin/s3_file.rb +0 -12
  63. data/lib/neuron-client/model/admin/zone.rb +0 -15
  64. data/lib/neuron-client/model/common/ad.rb +0 -42
  65. data/lib/neuron-client/model/common/ad_calculations.rb +0 -329
  66. data/lib/neuron-client/model/common/ad_zone.rb +0 -17
  67. data/lib/neuron-client/model/common/base.rb +0 -67
  68. data/lib/neuron-client/model/common/blocked_referer.rb +0 -16
  69. data/lib/neuron-client/model/common/blocked_user_agent.rb +0 -16
  70. data/lib/neuron-client/model/common/geo_target.rb +0 -16
  71. data/lib/neuron-client/model/common/pixel.rb +0 -18
  72. data/lib/neuron-client/model/common/report.rb +0 -21
  73. data/lib/neuron-client/model/common/s3_file.rb +0 -16
  74. data/lib/neuron-client/model/common/zone.rb +0 -22
  75. data/lib/neuron-client/model/common/zone_calculations.rb +0 -41
  76. data/lib/neuron-client/model/membase/ad.rb +0 -49
  77. data/lib/neuron-client/model/membase/ad_zone.rb +0 -11
  78. data/lib/neuron-client/model/membase/blocked_referer.rb +0 -20
  79. data/lib/neuron-client/model/membase/blocked_user_agent.rb +0 -20
  80. data/lib/neuron-client/model/membase/geo_target.rb +0 -11
  81. data/lib/neuron-client/model/membase/pixel.rb +0 -22
  82. data/lib/neuron-client/model/membase/report.rb +0 -11
  83. data/lib/neuron-client/model/membase/s3_file.rb +0 -11
  84. data/lib/neuron-client/model/membase/zone.rb +0 -30
  85. data/lib/neuron-client/model/models.rb +0 -15
  86. data/spec/lib/model/admin/ad_spec.rb +0 -34
  87. data/spec/lib/model/admin/ad_zone_spec.rb +0 -19
  88. data/spec/lib/model/admin/base_spec.rb +0 -11
  89. data/spec/lib/model/admin/blocked_referer_spec.rb +0 -11
  90. data/spec/lib/model/admin/blocked_user_agent_spec.rb +0 -11
  91. data/spec/lib/model/admin/geo_target_spec.rb +0 -30
  92. data/spec/lib/model/admin/report_spec.rb +0 -21
  93. data/spec/lib/model/admin/s3_spec.rb +0 -11
  94. data/spec/lib/model/admin/zone_spec.rb +0 -21
  95. data/spec/lib/model/common/ad_calculations_spec.rb +0 -1151
  96. data/spec/lib/model/common/ad_spec.rb +0 -11
  97. data/spec/lib/model/common/ad_zone_spec.rb +0 -11
  98. data/spec/lib/model/common/base_spec.rb +0 -11
  99. data/spec/lib/model/common/blocked_referer_spec.rb +0 -11
  100. data/spec/lib/model/common/blocked_user_agent_spec.rb +0 -11
  101. data/spec/lib/model/common/geo_target_spec.rb +0 -11
  102. data/spec/lib/model/common/report_spec.rb +0 -11
  103. data/spec/lib/model/common/s3_spec.rb +0 -11
  104. data/spec/lib/model/common/zone_calculations_spec.rb +0 -54
  105. data/spec/lib/model/common/zone_spec.rb +0 -11
  106. data/spec/lib/model/membase/ad_spec.rb +0 -54
  107. data/spec/lib/model/membase/ad_zone_spec.rb +0 -11
  108. data/spec/lib/model/membase/base_spec.rb +0 -11
  109. data/spec/lib/model/membase/blocked_referer_spec.rb +0 -34
  110. data/spec/lib/model/membase/blocked_user_agent_spec.rb +0 -34
  111. data/spec/lib/model/membase/geo_target_spec.rb +0 -11
  112. data/spec/lib/model/membase/pixel_spec.rb +0 -34
  113. data/spec/lib/model/membase/report_spec.rb +0 -11
  114. data/spec/lib/model/membase/s3_spec.rb +0 -11
  115. data/spec/lib/model/membase/zone_spec.rb +0 -32
  116. data/spec/lib/old_spec.rb +0 -437
@@ -1,37 +1,252 @@
1
1
  module Neuron
2
2
  module Client
3
- module Model
4
- class Base
5
- instance_methods.each { |m| undef_method m unless m =~ /(^__|^send$|^object_id|^class$)/ }
3
+ module Base
6
4
 
7
- def initialize(attrs=nil)
8
- @proxied_model = self.class.class_to_proxy.new(attrs)
5
+ module ClassAndInstanceMethods
6
+
7
+ def find(id)
8
+ if connected_to_membase?
9
+ key = cache_key(id)
10
+ connection.local_cache.fetch(key) do
11
+ cached_json = connection.get(key)
12
+ if cached_json.present?
13
+ data = Yajl.load(cached_json)
14
+ validate_against_schema!(:show, data)
15
+ from_hash(data)
16
+ end
17
+ end
18
+ elsif connected_to_admin?
19
+ data = connection.get("#{resources_name}/#{id}")
20
+ validate_against_schema!(:show, data)
21
+ from_hash(data)
22
+ else
23
+ raise "Not configured!"
24
+ end
25
+ end
26
+
27
+ def all
28
+ if connected_to_membase?
29
+ key = cache_key(":all")
30
+ connection.local_cache.fetch(key) do
31
+ cached_json = connection.get(resources_name)
32
+ if cached_json.present?
33
+ data = Yajl.load(cached_json)
34
+ validate_against_schema!(:index, data)
35
+ data.map{ |hash| from_hash(hash) }
36
+ else
37
+ []
38
+ end
39
+ end
40
+ elsif connected_to_admin?
41
+ data = connection.get(resources_name)
42
+ validate_against_schema!(:index, data)
43
+ data.map{ |hash| from_hash(hash) }
44
+ else
45
+ raise "Not configured!"
46
+ end
47
+ end
48
+
49
+ def create(attrs={})
50
+ @errors = catch :errors do
51
+ return create!(attrs)
52
+ end
53
+ nil
54
+ end
55
+
56
+ def create!(attrs={})
57
+ data = {resource_name => attrs.stringify_keys}
58
+ validate_against_schema!(:create, data)
59
+ data = connection.post("#{resources_name}", data)
60
+ validate_against_schema!(:show, data)
61
+ from_hash(data)
9
62
  end
10
63
 
11
- def method_missing(meth, *args, &block)
12
- (@proxied_model.respond_to?(meth) ? @proxied_model.send(meth, *args, &block) : super)
64
+ def api=(api)
65
+ @api = api
66
+ end
67
+
68
+ def validate=(validate)
69
+ @validate = validate
70
+ end
71
+
72
+ protected
73
+
74
+ ID_PATTERN = /\A\d+\z/.freeze
75
+ def validate_id!(id)
76
+ if validate?
77
+ unless (id.is_a?(Integer) || ID_PATTERN.match(id.to_s)) && id.to_i > 0
78
+ raise "Invalid ID: #{id.inspect}"
79
+ end
80
+ end
81
+ end
82
+
83
+ UUID_PATTERN = /\A[a-z0-9]+\z/.freeze
84
+ def validate_uuid!(uuid)
85
+ if validate?
86
+ unless UUID_PATTERN.match(uuid) && uuid.length <= 25
87
+ raise "Invalid UUID: #{uuid}"
88
+ end
89
+ end
90
+ rescue Exception => e
91
+ raise "Invalid UUID: #{uuid}"
13
92
  end
14
93
 
15
- class << self
16
- attr_accessor :api
17
- def api
94
+ # schema_name : one of [:index, :show, :create, update]
95
+ # data : the serializable object that should match the given schema
96
+ def validate_against_schema!(schema_name, data)
97
+ if validate? && data.present?
98
+ JSON::Validator.validate!(schema.send(schema_name), data)
99
+ end
100
+ end
101
+
102
+ def from_hash(hash)
103
+ if hash.present?
104
+ attrs = hash[resource_name]
105
+ klass.new(attrs) if attrs.present?
106
+ end
107
+ end
108
+
109
+ def api
110
+ if self.is_a?(Class)
18
111
  @api || Neuron::Client::API.default_api
112
+ else
113
+ @api || self.class.api
114
+ end
115
+ end
116
+
117
+ def validate?
118
+ return !!@validate unless @validate.nil?
119
+ if self.is_a?(Class)
120
+ api.validate?
121
+ else
122
+ self.class.validate?
19
123
  end
124
+ end
125
+
126
+ def connection
127
+ api.connection
128
+ end
129
+
130
+ def connected?
131
+ connection.present?
132
+ end
133
+
134
+ def connected_to_admin?
135
+ api.connection_type == :admin && connected?
136
+ end
137
+
138
+ def connected_to_membase?
139
+ api.connection_type == :membase && connected?
140
+ end
20
141
 
21
- def connection
22
- api.connection
142
+ def connected_to_admin!
143
+ raise("Not configured for admin server!") unless connected_to_admin?
144
+ end
145
+
146
+ def connected_to_membase!
147
+ raise("Not configured for membase server!") unless connected_to_membase?
148
+ end
149
+
150
+ def klass
151
+ @klass ||= (self.is_a?(Class) ? self : self.class)
152
+ end
153
+
154
+ def schema
155
+ @schema ||= "Neuron::Schema::#{klass.name.demodulize}::SCHEMA".constantize
156
+ end
157
+
158
+ def cache_key(id)
159
+ "#{klass.name.demodulize}:#{id}"
160
+ end
161
+
162
+ def resource_name
163
+ @resource_name ||= klass.name.demodulize.underscore
164
+ end
165
+
166
+ def resources_name
167
+ @resources_name ||= resource_name.pluralize
168
+ end
169
+ end
170
+
171
+ def self.included(base)
172
+ base.send(:attr_accessor, :errors) if base.is_a?(Class)
173
+ base.send(:attr_reader, :id) if base.is_a?(Class)
174
+ base.extend(ClassAndInstanceMethods)
175
+ end
176
+
177
+ include ClassAndInstanceMethods
178
+
179
+ def initialize(attrs={})
180
+ apply_attributes!(attrs)
181
+ end
182
+
183
+ def apply_attributes!(attrs)
184
+ if attrs.present?
185
+ attrs.each do |k,v|
186
+ self.send("#{k}=", v) if self.respond_to?("#{k}=")
23
187
  end
188
+ end
189
+ end
24
190
 
25
- def class_to_proxy
26
- module_to_load = api.connection_type.to_s.titleize
27
- class_name_to_load = name.split('::').last
28
- Neuron::Client::Model.const_get(module_to_load).const_get(class_name_to_load)
191
+ def to_hash(*except)
192
+ hash = {}
193
+ except = except.map(&:to_sym)
194
+ attributes.each do |attribute|
195
+ unless except.include?(attribute.to_sym)
196
+ value = send(attribute)
197
+ hash[attribute.to_s] = value
29
198
  end
199
+ end
200
+ {resource_name => hash}
201
+ end
202
+
203
+ def to_create_hash(*except)
204
+ to_hash(*([:errors, :updated_at, :created_at, :id] + except))
205
+ end
206
+
207
+ def to_update_hash(*except)
208
+ to_hash(*([:errors, :updated_at, :created_at] + except))
209
+ end
210
+
211
+ def new_record?
212
+ id.nil?
213
+ end
214
+
215
+ def id=(id)
216
+ @id = Integer(id)
217
+ end
218
+
219
+ def save
220
+ update_attributes
221
+ end
30
222
 
31
- def method_missing(meth, *args, &block)
32
- (class_to_proxy.respond_to?(meth) ? class_to_proxy.send(meth, *args, &block) : super)
223
+ def update_attributes(attrs={})
224
+ connected_to_admin!
225
+ @errors = catch :errors do
226
+ apply_attributes!(attrs) if attrs.present?
227
+ data = {}
228
+ if new_record?
229
+ data = to_create_hash
230
+ validate_against_schema!(:create, data)
231
+ data = connection.post(resources_name, data)
232
+ else
233
+ data = to_update_hash
234
+ validate_against_schema!(:update, data)
235
+ data = connection.put("#{resources_name}/#{id}", data)
33
236
  end
237
+ apply_attributes!(data[resource_name])
238
+ []
34
239
  end
240
+ @errors.empty?
241
+ end
242
+
243
+ def valid?
244
+ @errors.empty?
245
+ end
246
+
247
+ def destroy
248
+ connected_to_admin!
249
+ connection.delete("#{resources_name}/#{id}")
35
250
  end
36
251
  end
37
252
  end
@@ -0,0 +1,20 @@
1
+ module Neuron
2
+ module Client
3
+ class BlockedReferer
4
+ include Base
5
+
6
+ ATTRIBUTES = [
7
+ :id,
8
+ :referer, # string, URL
9
+ :created_at, # string, datetime in UTC
10
+ :updated_at, # string, datetime in UTC
11
+ ]
12
+
13
+ attr_accessor *ATTRIBUTES
14
+
15
+ def attributes
16
+ ATTRIBUTES
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ module Neuron
2
+ module Client
3
+ class BlockedUserAgent
4
+ include Base
5
+
6
+ ATTRIBUTES = [
7
+ :id,
8
+ :user_agent, # string, regex (max 2 GB length)
9
+ :description, # nil, or string (max 2 GB length)
10
+ :created_at, # string, datetime in UTC
11
+ :updated_at, # string, datetime in UTC
12
+ ]
13
+
14
+ attr_accessor *ATTRIBUTES
15
+
16
+ def attributes
17
+ ATTRIBUTES
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,36 @@
1
+ module Neuron
2
+ module Client
3
+ class GeoTarget
4
+ include Base
5
+
6
+ TYPES = %w(COUNTRY REGION METRO CITY) # DO NOT modify, unless you're absolutely sure of all the ramifications!
7
+
8
+ ATTRIBUTES = [
9
+ :id,
10
+ :geo_type, # string, one of TYPES
11
+ :net_acuity_id, # integer
12
+ :abbreviation, # string
13
+ :full_name, # string
14
+ :name, # string
15
+ ]
16
+
17
+ attr_accessor *ATTRIBUTES
18
+
19
+ def attributes
20
+ ATTRIBUTES
21
+ end
22
+
23
+ EXPECTED_QUERY_PARAMS = %w(geo_type search limit)
24
+ def self.query(parameters)
25
+ if validate?
26
+ unless parameters.all?{|k,v| EXPECTED_QUERY_PARAMS.include?(k.to_s)}
27
+ raise "Unsupported parameters: #{parameters.inspect}"
28
+ end
29
+ end
30
+ data = self.connection.get("geo_targets", parameters)
31
+ validate_against_schema!(:index, data)
32
+ data.map{ |hash| from_hash(hash) }
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,22 @@
1
+ module Neuron
2
+ module Client
3
+ class Pixel
4
+ include Base
5
+
6
+ ATTRIBUTES = [
7
+ :id,
8
+ :ad_ids, # array of integers
9
+ ]
10
+
11
+ attr_accessor *ATTRIBUTES
12
+
13
+ def attributes
14
+ ATTRIBUTES
15
+ end
16
+
17
+ def ad_ids
18
+ @ad_ids.nil? ? [] : @ad_ids
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,35 @@
1
+ module Neuron
2
+ module Client
3
+ class Report
4
+ include Base
5
+
6
+ TEMPLATES = %w(ad_events delivery_metrics post_activities)
7
+
8
+ STATES = %w(WAITING RUNNING READY FAILED CANCELLED)
9
+
10
+ ATTRIBUTES = [
11
+ :id,
12
+ :parameters, # hash, where keys are parameter names, and values are parameter values as strings.
13
+ :state, # string, one of STATES
14
+ :template, # string, one of TEMPLATES
15
+ :created_at, # string, datetime in UTC
16
+ :updated_at, # string, datetime in UTC
17
+ ]
18
+
19
+ attr_accessor *ATTRIBUTES
20
+
21
+ def attributes
22
+ ATTRIBUTES
23
+ end
24
+
25
+ def status
26
+ @state
27
+ end
28
+
29
+ def result
30
+ connected_to_admin!
31
+ connection.get("reports/#{id}/result", :format => "")
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,30 @@
1
+ module Neuron
2
+ module Client
3
+ class S3File
4
+ include Base
5
+
6
+ PURPOSE_CHOICES = [
7
+ "REPORT_RESULT",
8
+ "RAW_EVENT_LOG",
9
+ "NORMALIZED_EVENT_LOG",
10
+ "ENRICHED_EVENT_LOG"
11
+ ]
12
+
13
+ ATTRIBUTES = [
14
+ :id,
15
+ :bucket, # string, at least 3 chars
16
+ :filename, # string
17
+ :filesize, # nil, or integer
18
+ :purpose, # string, one of PURPOSE_CHOICES
19
+ :created_at, # string, datetime in UTC
20
+ :updated_at, # string, datetime in UTC
21
+ ]
22
+
23
+ attr_accessor *ATTRIBUTES
24
+
25
+ def attributes
26
+ ATTRIBUTES
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,64 @@
1
+ module Neuron
2
+ module Client
3
+ class Zone
4
+ include Base
5
+ include ZoneCalculations
6
+
7
+ ADS_BY_PRIORITY_TTL = 60 #seconds
8
+
9
+ REDIRECT = "Redirect"
10
+ IRIS = "Iris"
11
+ VAST = "Vast"
12
+ RESPONSE_TYPES = [REDIRECT, IRIS, VAST]
13
+
14
+ TEMPLATE_SLUGS = ["300x250", "300x600"]
15
+
16
+ ATTRIBUTES = [
17
+ :id,
18
+ :ad_links,
19
+ :name,
20
+ :response_type, # string in RESPONSE_TYPES
21
+ :template_slug, # nil, or string in TEMPLATE_SLUGS
22
+ :mute, # nil, or "Yes" or "No"
23
+ :autoplay, # nil, or "Yes" or "No"
24
+ :channel, # nil, or slug
25
+ :expand, # nil, or "Yes" or "No"
26
+ :text_overlay, # nil, or "Yes" or "No"
27
+ :nami_feed_url, # nil, or string URL
28
+ :created_at, # string, datetime in UTC
29
+ :updated_at, # string, datetime in UTC
30
+ ]
31
+
32
+ attr_accessor *ATTRIBUTES
33
+
34
+ def attributes
35
+ ATTRIBUTES
36
+ end
37
+
38
+ def id=(id)
39
+ @id = id.to_s
40
+ end
41
+
42
+ def find_ad(ad_id)
43
+ Ad.find(ad_id)
44
+ end
45
+
46
+ def unlink(ad_id)
47
+ connected_to_admin!
48
+ validate_id!(ad_id)
49
+ validate_uuid!(id)
50
+ connection.delete("zones/#{id}/ads/#{ad_id}")
51
+ end
52
+
53
+ def ads_by_priority
54
+ if connected_to_membase?
55
+ connection.fetch("Zone:#{id}:ads_by_priority", ADS_BY_PRIORITY_TTL) do
56
+ calculate_ads_by_priority
57
+ end
58
+ else
59
+ calculate_ads_by_priority
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,37 @@
1
+ module Neuron
2
+ module Client
3
+ module ZoneCalculations
4
+ # This module expects the following methods to be defined:
5
+ #
6
+ # ad_links (Hash, keys are ad IDs, values are a sub-hash: {'priority' => p, 'weight' => w})
7
+ # find_ad(ad_id) (nil, or an object that responds to :active? and :pressure)
8
+
9
+ def ads_by_priority
10
+ calculate_ads_by_priority
11
+ end
12
+
13
+ def calculate_ads_by_priority
14
+ entries = {}
15
+ ad_links.each do |ad_id, link|
16
+ next unless ad = find_ad(ad_id)
17
+ next unless ad.active?
18
+ weight = link['weight'].to_f
19
+ priority = link['priority'].to_f
20
+ entries[priority] ||= []
21
+ entries[priority] << [ad_id, weighted_pressure(weight, ad.pressure)]
22
+ end
23
+ entries.sort_by do |priority, entry|
24
+ priority
25
+ end.map do |priority, entry|
26
+ entry.sort_by(&:first)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def weighted_pressure(weight, pressure)
33
+ [( [weight, 0.0].max * [pressure, 1.0].max ), 1.0].max.to_f
34
+ end
35
+ end
36
+ end
37
+ end