housing_misc 0.5.4

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.
@@ -0,0 +1,27 @@
1
+ module Configuration
2
+
3
+ def configuration
4
+ yield self
5
+ end
6
+
7
+ def define_setting(name, default = nil)
8
+ class_variable_set("@@#{name}", default)
9
+
10
+ define_class_method "#{name}=" do |value|
11
+ class_variable_set("@@#{name}", value)
12
+ end
13
+
14
+ define_class_method name do
15
+ class_variable_get("@@#{name}")
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def define_class_method(name, &block)
22
+ (class << self; self; end).instance_eval do
23
+ define_method name, &block
24
+ end
25
+ end
26
+
27
+ end
@@ -0,0 +1,31 @@
1
+
2
+ require 'helpers/configuration'
3
+ module HousingMisc
4
+ extend Configuration
5
+
6
+ define_setting :aws_access_token
7
+ define_setting :aws_access_secret
8
+ define_setting :region
9
+ define_setting :bucket
10
+ define_setting :origin
11
+ define_setting :credentials_flag
12
+ define_setting :expose_headers
13
+ define_setting :max_age
14
+ define_setting :sidekiq_custom_threshold, {}
15
+ define_setting :sidekiq_message_url, "https://hooks.slack.com/services/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"
16
+ define_setting :app_name
17
+
18
+
19
+ require 'housing_misc/api_helper'
20
+ require 'housing_misc/engine'
21
+ require 'housing_misc/slice_helper'
22
+ require 'housing_misc/json_slice_render'
23
+ require 'housing_misc/boolean_evaluator'
24
+ require 'housing_misc/railtie'
25
+ require 'housing_misc/distance_unit'
26
+ require 'housing_misc/models_reflections_helper'
27
+ require 'housing_misc/aerospike_cache_extension'
28
+ require 'housing_misc/encrypt_decrypt'
29
+ require 'housing_misc/net_http_generic_request'
30
+ require 'housing_misc/diff_checker'
31
+ end
@@ -0,0 +1,100 @@
1
+ require 'connection_pool'
2
+ require 'active_support/cache/aerospike_store'
3
+ module HousingMisc
4
+ class AerospikeCacheExtension < ActiveSupport::Cache::AerospikeStore
5
+ READ_WRITE_BATCH_SIZE = 10
6
+ def initialize(options = {})
7
+ super
8
+ aerospike_connection_policy = ::Aerospike::ClientPolicy.new(::JSON.parse(::Figaro.env.aerospike_connection_policy).with_indifferent_access)
9
+ @conn_pool = ConnectionPool.new(size: 20) { ::Aerospike::Client.new(::JSON.parse(Housing.aerospike_hosts).collect {|host_port| host_port.join(':') }.join(','), policy: aerospike_connection_policy)}
10
+ end
11
+
12
+
13
+ def read_multi(*names)
14
+ results = {}
15
+ aerospike_cache_config = ::JSON.parse(::Figaro.env.aerospike_cache_config)
16
+ key_list = names.map {|key| Aerospike::Key.new(aerospike_cache_config['namespace'], aerospike_cache_config['set'], key) }
17
+ @conn_pool.with do |conn|
18
+ unformatted_rslt = []
19
+ key_list.each_slice(READ_WRITE_BATCH_SIZE) do |batch_names|
20
+ unformatted_rslt += conn.batch_get(batch_names)
21
+ end
22
+ names.each_with_index do |name, i|
23
+ if unformatted_rslt[i] != nil
24
+ results[name] = decrypt_data(unformatted_rslt[i].bins['cache_entry']).value
25
+ end
26
+ end
27
+ end
28
+ return results
29
+ end
30
+
31
+ def fetch(key, options=nil)
32
+ if block_given?
33
+ if !options.nil? and options[:force]
34
+ result = yield
35
+ write(key, result, options)
36
+ return result
37
+ end
38
+
39
+ cached_object = read(key, options)
40
+ return cached_object if cached_object != nil
41
+
42
+ result = yield
43
+ write(key, result, options)
44
+ return result
45
+
46
+ end
47
+ end
48
+
49
+ def fetch_multi(*names)
50
+ options = names.extract_options!
51
+ options = merged_options(options)
52
+ results = read_multi(*names, options)
53
+ names.map do |name|
54
+ results.fetch(name) do
55
+ value = yield name
56
+ write(name, value, options)
57
+ results[name] = value
58
+ end
59
+ end
60
+ results
61
+ end
62
+
63
+ def write_multi(hash)
64
+ hash.each { |key, value| write(key, value.to_json) }
65
+ end
66
+
67
+ protected
68
+
69
+ def read_entry(key, options)
70
+ if value = internal_read_entry(key, options)
71
+ # if it is not raw it is a marshalled ActiveSupport::Cache::Entry
72
+ value = options[:raw]? ActiveSupport::Cache::Entry.new(value) : decrypt_data(value)
73
+ else
74
+ nil
75
+ end
76
+ end
77
+
78
+ def write_entry(key, entry, options)
79
+ options[:expiration] ||= options[:expires_in] if options[:expires_in]
80
+ options[:record_exists_action] ||= options[:unless_exist]? Aerospike::RecordExistsAction::CREATE_ONLY : Aerospike::RecordExistsAction::REPLACE
81
+ value = options[:raw]? entry.value : Marshal.dump(entry)
82
+ begin
83
+ @client.put(as_key(key, options), {options[:bin] => Zlib::Deflate.deflate(value)}, options)
84
+ rescue Aerospike::Exceptions::Aerospike => e
85
+ raise unless (e.result_code == Aerospike::ResultCode::KEY_EXISTS_ERROR)
86
+ false
87
+ end
88
+ end
89
+
90
+
91
+ def decrypt_data(data)
92
+ begin
93
+ return nil if data == nil
94
+ return Marshal.load(Zlib::Inflate.inflate(data))
95
+ rescue Zlib::DataError => e
96
+ return nil
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,239 @@
1
+ module HousingMisc::ApiHelper
2
+ require 'openssl'
3
+ MOBILE_REQUEST_TIME_THRESHOLD = 300 #in seconds
4
+
5
+ def get_request_attribute_value(headers, params, key)
6
+ headers["HTTP_#{key.upcase.tr('-', '_')}"] || params[key]
7
+ end
8
+
9
+ def filter_parameters(params)
10
+ filters = Rails.application.config.filter_parameters rescue []
11
+ f = ActionDispatch::Http::ParameterFilter.new filters
12
+ return f.filter(params)
13
+ end
14
+
15
+ def is_request_internal?(request)
16
+ Rails.env != "production" || request.host.include?("internal")
17
+ end
18
+
19
+ def is_request_from_mobile?(request, params, velocity_salt)
20
+ source = get_request_attribute_value(request.headers, params, "source") || ""
21
+ time_stamp = get_request_attribute_value(request.headers, params, "ts")
22
+ signed_param = get_request_attribute_value(request.headers, params, "sp")
23
+
24
+ if time_stamp.blank? || signed_param.blank?
25
+ if ["android", "ios"].include?(source.downcase) # To be removed once all the users moves to latest app version which includes ts-sp thing
26
+ log_file ||= Logger.new("#{Rails.root}/log/app_requests_with_missing_params.log")
27
+ log_invalid_csrf_request(log_file, request, cookies, params)
28
+ return true
29
+ elsif params["app_name"] == "locon.com.datacollection" # To be removed once dc app implements ts-sp thing
30
+ log_file ||= Logger.new("#{Rails.root}/log/dc_app_requests_with_missing_params.log")
31
+ log_invalid_csrf_request(log_file, request, cookies, params)
32
+ return true
33
+ end
34
+ else
35
+ return ((Time.now.to_i - time_stamp.to_i) <= MOBILE_REQUEST_TIME_THRESHOLD) && (Digest::MD5.hexdigest(velocity_salt + time_stamp) == signed_param)
36
+ end
37
+ return false
38
+ end
39
+
40
+ def is_request_csrf_valid?(request, cookies, params, csrf_encryption_key, velocity_salt)
41
+ if request.get? || is_request_internal?(request) || is_request_from_mobile?(request, params, velocity_salt)
42
+ return true
43
+ else
44
+ csrf_id = cookies["cuid"] || ""
45
+ csrf_token = request.headers["HTTP_X_CSRF_TOKEN_V2"]
46
+ return OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), csrf_encryption_key, csrf_id) == csrf_token
47
+ end
48
+ end
49
+
50
+ def internal_host_check
51
+ unless is_request_internal?(request)
52
+ log_file ||= Logger.new("#{Rails.root}/log/unauthorized_api_requests.log")
53
+ log_file.info "Internal host check violated | remote_ip:#{request.remote_ip}, host:#{request.host}, port:#{request.port}, params:#{filter_parameters(params)}"
54
+ render :json => {:message => "You are not authorized to make this call"}, status: 401
55
+ end
56
+ end
57
+
58
+ def csrf_check(csrf_encryption_key, velocity_salt)
59
+ unless is_request_csrf_valid?(request, cookies, params, csrf_encryption_key, velocity_salt)
60
+ log_file ||= Logger.new("#{Rails.root}/log/unauthorized_api_requests.log")
61
+ log_invalid_csrf_request(log_file, request, cookies, params)
62
+ render :json => {:message => "You are not authorized to make this call", :error => "CSRF_CHECK_FAILED"}, status: 401
63
+ end
64
+ end
65
+
66
+ def log_invalid_csrf_request(log_file, request, cookies, params)
67
+ log_file.info "CSRF check violated | new_csrf_token:#{request.headers["HTTP_X_CSRF_TOKEN_V2"]} | new_csrf_id:#{cookies["cuid"]} | remote_ip:#{request.remote_ip} | host:#{request.host} | port:#{request.port} | method:#{request.method} | referrer:#{request.referrer} | User-Agent:#{request.headers["User-Agent"]} | source:#{get_request_attribute_value(request.headers, params, "source")} | time_stamp: #{get_request_attribute_value(request.headers, params, "ts")} | signed_param:#{get_request_attribute_value(request.headers, params, "sp")} | params:#{filter_parameters(params)}"
68
+ end
69
+
70
+ def send_generic_mail(template_slug, template_version, users, base_vars, merge_vars, high_priority=false, attachments=[], deferred=false, send_time=nil, google_analytics_campaign=nil)
71
+ base_vars = base_vars.with_indifferent_access
72
+ merge_vars = merge_vars.with_indifferent_access
73
+ email_template_hash = get_email_template_from_mail_service(template_slug, template_version).with_indifferent_access
74
+ email_template_hash = add_base_vars(email_template_hash, base_vars)
75
+ if google_analytics_campaign.present?
76
+ email_template_hash[:params_json].merge!(google_analytics_campaign: google_analytics_campaign)
77
+ end
78
+ email_template_hash = add_user_emails(email_template_hash, users)
79
+ email_template_hash = add_global_merge_vars(email_template_hash, merge_vars)
80
+ email_template_hash = add_merge_vars(email_template_hash, merge_vars)
81
+ email_template_hash = add_attachments(email_template_hash, attachments)
82
+ if deferred
83
+ response = Net::HTTP.post_form( URI( "#{Housing.mail_service_url}v1/email/send_deferred" ), "email_request"=> email_template_hash.to_json, "high_priority"=>high_priority, "send_time" => send_time)
84
+ else
85
+ response = Net::HTTP.post_form( URI( "#{Housing.mail_service_url}v1/email/send" ), "email_request"=> email_template_hash.to_json, "high_priority"=>high_priority)
86
+ end
87
+ return response.code.to_i, response.body
88
+ end
89
+
90
+ def add_base_vars(email_template_hash, base_vars)
91
+ email_template_hash[:params_json].merge!(base_vars)
92
+ email_template_hash[:test_email] = Housing.mail_test_id
93
+ return email_template_hash
94
+ end
95
+
96
+ def add_user_emails(email_template_hash, users)
97
+ email_template_hash[:params_json][:users] = []
98
+ users.each do |user_email|
99
+ user_email = user_email.with_indifferent_access
100
+ email_template_hash[:params_json][:users].push({name: user_email[:name], email: user_email[:email]})
101
+ end
102
+ return email_template_hash
103
+ end
104
+
105
+ def add_global_merge_vars(email_template_hash, vars)
106
+ email_template_hash[:params_json][:global_merge_vars][:modules].each do |mod_name|
107
+ mod_name[:merge_vars].first.each do |key, value|
108
+ mod_name[:merge_vars].first[key] = vars[key] if value.nil?
109
+ end
110
+ end
111
+ return email_template_hash
112
+ end
113
+
114
+ def add_merge_vars(email_template_hash, vars)
115
+ email_template_hash[:params_json][:merge_vars] = []
116
+ email_template_hash[:params_json][:users].each do |user_email|
117
+ email = user_email[:email]
118
+ email_template_hash[:params_json][:merge_vars].push(email_template_hash[:params_json][:global_merge_vars].merge({email: email}))
119
+ end
120
+ if email_template_hash[:params_json][:global_merge_vars][:template_merge_vars].present?
121
+ email_template_hash[:params_json][:global_merge_vars][:template_merge_vars].each do |key, value|
122
+ email_template_hash[:params_json][:global_merge_vars][:template_merge_vars][key] = vars[key]
123
+ end
124
+ end
125
+ return email_template_hash
126
+ end
127
+
128
+ def add_attachments(email_template_hash, attachments)
129
+ email_template_hash[:params_json][:attachments] = attachments if attachments.present?
130
+ return email_template_hash
131
+ end
132
+
133
+
134
+ def get_email_template_from_mail_service(template_slug, template_version)
135
+ template_name = template_slug.gsub("-","_")
136
+ uri = URI.parse("#{Housing.mail_service_url}v1/email/request_format?template_name=#{template_name}&template_version=#{template_version}")
137
+ response = (Oj.load(Net::HTTP.get(uri))["data"]) rescue {}
138
+ end
139
+
140
+ def get_past_channel_details(profiles_hash, start_datetime, end_datetime, channel, template_details)
141
+ response = {}
142
+ query_params = {
143
+ template_name: template_details[:template_name],
144
+ end_datetime: end_datetime
145
+ }
146
+ query_params[:template_version] = template_details[:template_version] if channel == "email"
147
+
148
+ profiles_date_hash = {}
149
+ profiles_hash.each do |key, value|
150
+ date = value["last_activity_time"] || start_datetime
151
+ date = Date.strptime(date.to_s,'%s').to_time.to_i
152
+ (profiles_date_hash[date] ||= []) << key
153
+ end
154
+
155
+ profiles_date_hash.each do |date, email_or_numbers|
156
+ # increasing batch size would lead to exception 414 URI too large, Please don't increase batch size
157
+ email_or_numbers.each_slice(100) do |batch|
158
+ query_params[identifier[channel.to_sym]] = batch.join(',')
159
+ query_params[:start_datetime] = date
160
+ query = query_params.to_query
161
+ url = send("#{channel}_report_url".to_sym) + query
162
+ batch_response = (Oj.load(get_api_call(url))["data"])
163
+ response.merge!(batch_response)
164
+ end
165
+ end
166
+ response
167
+ end
168
+
169
+ def get_api_call(url, request_timeout = 15.0)
170
+ parsed_url = URI.parse(url)
171
+ http = Net::HTTP.new(parsed_url.host, parsed_url.port)
172
+ http.read_timeout = request_timeout
173
+ request = Net::HTTP::Get.new(parsed_url.request_uri)
174
+ begin
175
+ response = http.request(request)
176
+ raise "request failed :: #{response}" if response.code != "200"
177
+ request_response = response.body
178
+ rescue => exception
179
+ logging_file.error("CampaignError :: #{exception}")
180
+ raise "CampaignError for url: #{url}"
181
+ end
182
+ return request_response
183
+ end
184
+
185
+ def logging_file
186
+ @log_file ||= Logger.new("#{Rails.root}/log/campaign_errors.log")
187
+ end
188
+
189
+ def send_generic_sms(phone_number, template_name, send_time, sms_params)
190
+ post_args ={
191
+ "template_name" => template_name,
192
+ "sms_request[number]" => phone_number,
193
+ "sms_request[test_number]" => Housing.sms_test_number,
194
+ "send_time" => send_time,
195
+ "sms_request[params_json]" => sms_params.to_json
196
+ }
197
+ response = Net::HTTP.post_form( URI("#{Housing.sms_service_url}/v0/sms/send_deferred"), post_args)
198
+ end
199
+
200
+ def get_shortened_url(url)
201
+ shortened_url = ""
202
+ uri = URI.parse("#{Housing.url_shortener}/shorten")
203
+ response = Net::HTTP.post_form(uri, {url: url})
204
+ if response.code.to_i == 200
205
+ shortened_url = Housing.short_url_domain + JSON.parse(response.body)["id"]
206
+ end
207
+ shortened_url
208
+ end
209
+
210
+ def upload_log_to_s3(file_path)
211
+ s3_key = Rails.root.to_s.split('/')[-1] + '/' + file_path.split('/')[-1]
212
+ s3_client.bucket(HousingMisc.bucket).object(s3_key).upload_file(file_path)
213
+ end
214
+
215
+ private
216
+
217
+ def email_report_url
218
+ "#{Housing.mail_service_url}/api/v1/email-report?"
219
+ end
220
+
221
+ def sms_report_url
222
+ "#{Housing.sms_service_url}/v0/sms-report?"
223
+ end
224
+
225
+ def identifier
226
+ {
227
+ email: :emails,
228
+ sms: :phone_numbers
229
+ }
230
+ end
231
+
232
+ def s3_client
233
+ @s3_client ||= Aws::S3::Resource.new(
234
+ credentials: Aws::Credentials.new(HousingMisc.aws_access_token, HousingMisc.aws_access_secret),
235
+ region: HousingMisc.region
236
+ )
237
+ end
238
+
239
+ end
@@ -0,0 +1,7 @@
1
+ module BooleanEvaluator
2
+ def self.eval(val)
3
+ return true if val == true || val.to_s =~ (/^(true|t|yes|y|1|on)$/i)
4
+ return false if val == false || val.nil? || val.to_s =~ (/^(false|f|no|n|0|off)$/i)
5
+ raise ArgumentError.new("invalid value for Boolean: \"#{val}\"")
6
+ end
7
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'json'
5
+
6
+ # DiffChecker module is used to compare diff among
7
+ # application.yml application.yml.sample and default.rb.
8
+ module DiffChecker
9
+
10
+ def self.check_diff(deployment_file_path, sample_file_path, default_rb_file)
11
+ content = YAML.safe_load(File.read(deployment_file_path))
12
+ content_sample = YAML.safe_load(File.read(sample_file_path))
13
+ raise "#{sample_file_path} cant be empty" if (content_sample.nil? || content_sample.empty?)
14
+ missing_keys = content.nil? ? content_sample.keys : content_sample.keys - content.keys
15
+ unless missing_keys.empty?
16
+ raise "Some keys are missing in application.yml, but present in application.yml.sample. missing_keys : #{missing_keys}"
17
+ end
18
+ default_content_type1 = File.readlines(default_rb_file)
19
+ .map{ |line| line.match(/Figaro.env.[a-z0-9\_\-]+/).to_s.split('.')[2]}.reject{|line| line.nil?}
20
+ default_content_type2 = File.readlines(default_rb_file)
21
+ .map{ |line| line.match(/Figaro.env[^\.\s]+/).to_s.match(/[\"\'][a-z]*[^\]]+/)
22
+ .to_s.gsub!(/\A"|"\Z/, '')}.reject{|line| line.nil?}
23
+ default_file_keys = default_content_type1 + default_content_type2
24
+ missing_keys = content.nil? ? default_file_keys : default_file_keys - content.keys
25
+ unless missing_keys.empty?
26
+ raise "Some keys are missing in application.yml, but present in default.rb. missing_keys : #{missing_keys}"
27
+ end
28
+ end
29
+ end
30
+
@@ -0,0 +1,140 @@
1
+ METRIC_PREFIXES = {milli: 10**-3, centi: 10**-2, deci: 10**-1, '': 1, deka: 10**1, hecto: 10**2, kilo: 10**3}
2
+ IMPERIAL_UNITS = {inches: 12**-1, feet: 1, yards: 3, miles: 5280}
3
+ DIMENSIONS = {'': 1, square_: 2, cubic_: 3}
4
+ FEET_TO_METRES_CONVERSION = 0.3048
5
+
6
+ require 'active_support/inflector'
7
+
8
+ Numeric.class_eval do
9
+ DIMENSIONS.each do |dimension_prefix, dimension|
10
+ METRIC_PREFIXES.keys.each do|prefix|
11
+ define_method("#{dimension_prefix}#{prefix}metres") do
12
+ distance_ob = MetricDistance.new(self, prefix.intern, dimension, dimension_prefix)
13
+ return distance_ob
14
+ end
15
+ alias_method "#{dimension_prefix}#{prefix}metre".intern, "#{dimension_prefix}#{prefix}metres".intern
16
+ end
17
+ end
18
+
19
+ DIMENSIONS.each do |dimension_prefix, dimension|
20
+ IMPERIAL_UNITS.keys.each do |base_unit|
21
+ define_method("#{dimension_prefix}#{base_unit}") do
22
+ distance_ob = ImperialDistance.new(self, base_unit, dimension, dimension_prefix)
23
+ return distance_ob
24
+ end
25
+ alias_method "#{dimension_prefix}#{base_unit.to_s.singularize}".intern, "#{dimension_prefix}#{base_unit}".intern
26
+ end
27
+ alias_method "#{dimension_prefix}foot".intern, "#{dimension_prefix}feet".intern
28
+ end
29
+
30
+ end
31
+
32
+ class DistanceUnit < Numeric
33
+
34
+ attr_reader :dimension, :dimension_prefix
35
+ attr_accessor :value, :base_unit
36
+
37
+ def initialize value, base_unit, dimension, dimension_prefix
38
+ @value = value
39
+ @base_unit = base_unit
40
+ @dimension = dimension
41
+ @dimension_prefix = dimension_prefix
42
+ end
43
+
44
+ def format_display
45
+ display_string = ''
46
+ base_distance = self.to_metres
47
+ if base_distance.value >= 1000
48
+ display_string = "#{base_distance.to_kilometres.value.round(1)} km"
49
+ else
50
+ display_string = "#{base_distance.value.round(0)} m"
51
+ end
52
+ return display_string
53
+ end
54
+
55
+ def unit
56
+ return "#{self.dimension_prefix}#{self.base_unit}"
57
+ end
58
+
59
+ def value
60
+ return @value.to_f
61
+ end
62
+
63
+ DIMENSIONS.each do |dimension_prefix, dimension|
64
+ METRIC_PREFIXES.each do |prefix, scale|
65
+ define_method("to_#{dimension_prefix}#{prefix}metres") do
66
+ raise "Dimension Error" if self.dimension != dimension
67
+ base_imperial = self.send("to_#{dimension_prefix}feet")
68
+ metric_value = base_imperial.value * (FEET_TO_METRES_CONVERSION ** dimension)
69
+ base_metric = MetricDistance.new(metric_value, '', dimension, dimension_prefix)
70
+ metric_distance = base_metric.send("to_#{dimension_prefix}#{prefix}metres")
71
+ end
72
+ alias_method "to_#{dimension_prefix}#{prefix}metre".intern, "to_#{dimension_prefix}#{prefix}metres".intern
73
+ end
74
+ end
75
+
76
+ DIMENSIONS.each do |dimension_prefix, dimension|
77
+ IMPERIAL_UNITS.each do |base_unit, scale|
78
+ define_method("to_#{dimension_prefix}#{base_unit}") do
79
+ raise "Dimension Error" if self.dimension != dimension
80
+ base_metric = self.send("to_#{dimension_prefix}metres")
81
+ imperial_value = base_metric.value * (FEET_TO_METRES_CONVERSION ** -dimension)
82
+ base_imperial = ImperialDistance.new(imperial_value, :feet, dimension, dimension_prefix)
83
+ imperial_distance = base_imperial.send("to_#{dimension_prefix}#{base_unit}")
84
+ end
85
+ alias_method "to_#{dimension_prefix}#{base_unit.to_s.singularize}".intern, "to_#{dimension_prefix}#{base_unit}".intern
86
+ end
87
+ alias_method "to_#{dimension_prefix}foot".intern, "to_#{dimension_prefix}feet".intern
88
+ end
89
+
90
+ end
91
+
92
+ class MetricDistance < DistanceUnit
93
+
94
+ attr_accessor :metric_prefix
95
+
96
+ def initialize value, metric_prefix, dimension, dimension_prefix
97
+ @metric_prefix = metric_prefix
98
+ base_unit = "#{metric_prefix}metres".intern
99
+ super(value, base_unit, dimension, dimension_prefix)
100
+ end
101
+
102
+ def metric_prefix= metric_prefix
103
+ self.metric_prefix = metric_prefix
104
+ self.base_unit = "#{metric_prefix}metres".intern
105
+ end
106
+
107
+ DIMENSIONS.each do |dimension_prefix, dimension|
108
+ METRIC_PREFIXES.each do |prefix, scale|
109
+ define_method("to_#{dimension_prefix}#{prefix}metres") do
110
+ raise "Dimension Error" if self.dimension != dimension
111
+ current_value = self.value
112
+ current_prefix = self.metric_prefix
113
+ new_value = current_value * ((METRIC_PREFIXES[current_prefix.intern] * (scale**-1)) ** dimension)
114
+ return MetricDistance.new(new_value, prefix.intern, dimension, dimension_prefix)
115
+ end
116
+ alias_method "to_#{dimension_prefix}#{prefix}metre".intern, "to_#{dimension_prefix}#{prefix}metres".intern
117
+ end
118
+ end
119
+
120
+ end
121
+
122
+ class ImperialDistance < DistanceUnit
123
+
124
+ DIMENSIONS.each do |dimension_prefix, dimension|
125
+ IMPERIAL_UNITS.each do |base_unit, scale|
126
+ define_method("to_#{dimension_prefix}#{base_unit}") do
127
+ raise "Dimension Error" if self.dimension != dimension
128
+ current_value = self.value
129
+ current_unit = self.base_unit
130
+ new_value = current_value * ((IMPERIAL_UNITS[current_unit.intern] * (scale**-1)) ** dimension)
131
+ return ImperialDistance.new(new_value, base_unit, dimension, dimension_prefix)
132
+ end
133
+ alias_method "to_#{dimension_prefix}#{base_unit.to_s.singularize}".intern, "to_#{dimension_prefix}#{base_unit}".intern
134
+ end
135
+ alias_method "to_#{dimension_prefix}foot".intern, "to_#{dimension_prefix}feet".intern
136
+ end
137
+
138
+ end
139
+
140
+