haystack_ruby 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1bbe8505587fa1e720665305767e15d16a78a11b
4
+ data.tar.gz: 8ddbb550be5df6a595b1dd3740f6405c3acf9685
5
+ SHA512:
6
+ metadata.gz: 93f07240acd1a2a50c41b2a9e27789c1b8d1cff16e4a2825d9f4577e7b345cedbe7581fe8b5370dffacb4e5885b6a9f30f7e8d371ade56017788a117a6f3b6c2
7
+ data.tar.gz: bc73973de118ee8a52e9dab9efc59ba9486b048d6bba07d9656c277e19a9610dbaaad5527362f14008b1e9e49c7fcffd471604fc0e4d9923d693821d360c3236
@@ -0,0 +1,184 @@
1
+ require 'securerandom' #consider using openssl random
2
+ require 'base64'
3
+ require 'openssl'
4
+ module HaystackRuby
5
+ module Auth
6
+ module Scram
7
+ class Conversation
8
+ attr_reader :auth_token, :nonce, :server_nonce, :server_salt
9
+
10
+ def initialize(user, url)
11
+ @user = user
12
+ @url = url
13
+ @nonce = SecureRandom.base64.tr('=','') #TODO check if problem to potentially strip =
14
+ @digest = OpenSSL::Digest::SHA256.new
15
+ @handshake_token = Base64.strict_encode64(@user.username)
16
+ end
17
+
18
+ def authorize
19
+ res = send_first_message
20
+ parse_first_response(res)
21
+ res = send_second_message
22
+ parse_second_response(res)
23
+ end
24
+
25
+ def connection
26
+ @connection ||= Faraday.new(:url => @url) do |faraday|
27
+ # faraday.response :logger # log requests to STDOUT
28
+ faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
29
+ faraday.headers['Accept'] = 'application/json' #TODO enable more formats
30
+ faraday.headers['Content-Type'] = 'text/plain'
31
+ end
32
+ end
33
+
34
+ # first message sent by client to server
35
+ def send_first_message
36
+ res = connection.get('about') do |req|
37
+ req.headers['Authorization'] = "SCRAM handshakeToken=#{@handshake_token},data=#{Base64.urlsafe_encode64(first_message).tr('=','')}"
38
+ end
39
+ res
40
+
41
+ end
42
+
43
+ # pull server data out of response to first message
44
+ def parse_first_response(response)
45
+
46
+ # parse server response to first message
47
+ response_str = response.env.response_headers['www-authenticate']
48
+ unless response_str.index('scram ') == 0
49
+ throw 'Invalid response from server'
50
+ end
51
+ response_str.slice! 'scram '
52
+ response_vars = {}
53
+ response_str.split(', ').each do |pair|
54
+ key,value = pair.split('=')
55
+ response_vars[key] = value
56
+ end
57
+ unless response_vars['hash'] == 'SHA-256'
58
+ throw "Server requested unsupported hash algorithm: #{response_vars['hash']}"
59
+ end
60
+
61
+ # todo check handshake token (should be base64 encode of username)
62
+
63
+ @server_first_msg = Base64.decode64(response_vars["data"])
64
+ server_vars = {}
65
+ @server_first_msg.split(',').each do |pair|
66
+ key,value = pair.split '='
67
+ server_vars[key] = value
68
+ end
69
+ @server_nonce = server_vars['r']
70
+ @server_salt = server_vars['s'] #Base64.decode64(server_vars['s'])
71
+ @server_iterations = server_vars['i'].to_i
72
+ end
73
+
74
+ def send_second_message
75
+ res = connection.get('about') do |req|
76
+ req.headers['Authorization'] = "SCRAM handshakeToken=#{@handshake_token},data=#{Base64.strict_encode64(client_final).tr('=','')}"
77
+ end
78
+ res
79
+
80
+ end
81
+ def parse_second_response(response)
82
+ begin
83
+ response_str = response.env.response_headers['authentication-info']
84
+ response_vars = {}
85
+ response_str.split(', ').each do |pair|
86
+ key,value = pair.split('=')
87
+ response_vars[key] = value
88
+ end
89
+ # decode data attribute to check server signature is as expected
90
+ key,val = Base64.decode64(response_vars['data']).split('=')
91
+ response_vars[key] = val
92
+ server_sig = response_vars['v']
93
+ unless server_sig == expected_server_signature
94
+ throw "invalid signature from server"
95
+ end
96
+ @auth_token = response_vars['authToken']
97
+ # rescue Exception => e
98
+ # raise
99
+ end
100
+
101
+ end
102
+
103
+ def test_auth_token
104
+ res = connection.get('about') do |req|
105
+ req.headers['Authorization'] = "BEARER authToken=#{@auth_token}"
106
+ end
107
+ end
108
+
109
+ private
110
+
111
+ # utility methods, closely matched with SCRAM notation and algorithm overview here:
112
+ # https://tools.ietf.org/html/rfc5802#section-3
113
+
114
+ def auth_message
115
+ @auth_message ||= "#{first_message},#{@server_first_msg},#{without_proof}"
116
+ end
117
+
118
+ def client_final
119
+ @client_final ||= "#{without_proof},p=" +
120
+ client_proof(client_key, client_signature(stored_key(client_key), auth_message))
121
+ end
122
+
123
+ def client_key
124
+ @client_key ||= hmac(salted_password, 'Client Key')
125
+ end
126
+
127
+ def client_proof(key, signature)
128
+ @client_proof ||= Base64.strict_encode64(xor(key, signature))
129
+ end
130
+
131
+ def client_signature(key, message)
132
+ @client_signature ||= hmac(key, message)
133
+ end
134
+
135
+ def expected_server_key
136
+ @server_key ||= hmac(salted_password, 'Server Key')
137
+ end
138
+
139
+ def expected_server_signature
140
+ @server_signature ||= Base64.strict_encode64(hmac(expected_server_key, auth_message)).tr('=','')
141
+ end
142
+
143
+ def first_message
144
+ "n=#{@user.username},r=#{@nonce}"
145
+ end
146
+
147
+ def h(string)
148
+ @digest.digest(string)
149
+ end
150
+
151
+ def hi(data)
152
+ OpenSSL::PKCS5.pbkdf2_hmac(
153
+ data,
154
+ Base64.decode64(@server_salt),
155
+ @server_iterations,
156
+ @digest.digest_length,
157
+ @digest
158
+ )
159
+ end
160
+
161
+ def hmac(data, key)
162
+ OpenSSL::HMAC.digest(@digest,data,key)
163
+ end
164
+
165
+
166
+ def salted_password
167
+ @salted_password ||= hi(@user.password)
168
+ end
169
+
170
+ def stored_key(key)
171
+ h(key)
172
+ end
173
+
174
+ def without_proof
175
+ @without_proof ||= "c=biws,r=#{@server_nonce}"
176
+ end
177
+
178
+ def xor(first, second)
179
+ first.bytes.zip(second.bytes).map{ |(a,b)| (a ^ b).chr }.join('')
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,22 @@
1
+ module HaystackRuby
2
+ module Config
3
+ extend self
4
+ attr_accessor :projects
5
+
6
+ def load_configuration conf
7
+ @projects = {}
8
+ conf.each do |name, config|
9
+ p = Project.new(name, config)
10
+ @projects[name] = p if p.valid?
11
+ end
12
+ end
13
+
14
+ # called in railtie
15
+ def load!(path, environment = nil)
16
+ require 'yaml'
17
+ environment ||= Rails.env
18
+ conf = YAML.load(File.new(path).read).with_indifferent_access[environment]
19
+ load_configuration(conf)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ module HaystackRuby
2
+ class Error < StandardError
3
+
4
+ end
5
+ end
@@ -0,0 +1,70 @@
1
+ require 'haystack_ruby/types/bin'
2
+ require 'haystack_ruby/types/coord'
3
+ require 'haystack_ruby/types/date'
4
+ require 'haystack_ruby/types/date_time'
5
+ require 'haystack_ruby/types/marker'
6
+ require 'haystack_ruby/types/n_a'
7
+ require 'haystack_ruby/types/number'
8
+ require 'haystack_ruby/types/ref'
9
+ require 'haystack_ruby/types/str'
10
+ require 'haystack_ruby/types/time'
11
+ require 'haystack_ruby/types/uri'
12
+
13
+ module HaystackRuby
14
+ class Object
15
+ # Always present
16
+ attr_reader :value, :haystack_type
17
+ # May be present, depending on type
18
+ attr_reader :unit, :description
19
+ # initialize with a encoded json string
20
+ def initialize val
21
+ case val
22
+ when Array
23
+ @haystack_type = "Array" #may want to decode array components?
24
+ @value = val
25
+ when TrueClass
26
+ @haystack_type = "Boolean"
27
+ @value = val
28
+ when FalseClass
29
+ @haystack_type = "Boolean"
30
+ @value = val
31
+ when NilClass
32
+ @haystack_type = 'Null'
33
+ @value = val
34
+ when String
35
+ # Map to Haystack type per http://project-haystack.org/doc/Json
36
+ case val
37
+ when /\Am:.*/
38
+ include HaystackRuby::Types::Marker
39
+ when /\Az:.*/
40
+ include HaystackRuby::Types::NA
41
+ when /\An:.*/
42
+ extend HaystackRuby::Types::Number
43
+ when /\Ar:.*/
44
+ include HaystackRuby::Types::Ref
45
+ when /\As:.*/
46
+ include HaystackRuby::Types::Str
47
+ when /\Ad:.*/
48
+ include HaystackRuby::Types::Date
49
+ when /\Ah:.*/
50
+ include HaystackRuby::Types::Time
51
+ when /\At:.*/
52
+ include HaystackRuby::Types::DateTime
53
+ when /\Au:.*/
54
+ include HaystackRuby::Types::Uri
55
+ when /\Ab:.*/
56
+ include HaystackRuby::Types::Bin
57
+ when /\Ac:.*/
58
+ include HaystackRuby::Types::Coord
59
+ when /\Ax:.*/
60
+ raise HaystackRuby::Error, "parsing of XStr type is not supported for string val #{val}"
61
+ else
62
+ raise HaystackRuby::Error, "unrecognized type for string val #{val}"
63
+ end
64
+ else
65
+ raise HaystackRuby::Error, "unrecognized type for val #{val}"
66
+ end
67
+ set_fields val
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,80 @@
1
+ require 'date'
2
+ # require 'active_support'
3
+ module HaystackRuby
4
+ module Point
5
+
6
+ # is this Point valid for purposees of Project Haystack Integration?
7
+ def haystack_valid?
8
+ return self.haystack_project_name.present? && self.haystack_point_id.present? && self.haystack_time_zone.present?
9
+ end
10
+
11
+ def haystack_project
12
+ @project ||= HaystackRuby::Config.projects[self.haystack_project_name]
13
+ end
14
+
15
+ def connection
16
+ haystack_project.connection
17
+ end
18
+
19
+ def his_read(range)
20
+ query = ["ver:\"#{haystack_project.haystack_version}\"",'id,range',"@#{self.haystack_point_id},\"#{range}\""]
21
+ pp query.join "\n"
22
+ res = connection.post('hisRead') do |req|
23
+ req.headers['Content-Type'] = 'text/plain'
24
+ req.body = query.join("\n")
25
+ end
26
+ JSON.parse! res.body
27
+ end
28
+
29
+ def meta_data
30
+ # read request on project to load current info, including tags and timezone
31
+ res = haystack_project.read({:id => "@#{self.haystack_point_id}"})['rows'][0]
32
+ end
33
+
34
+ # data is ascending array of hashes with format: {time: epochtime, value: myvalue}
35
+ def his_write(data)
36
+ query =
37
+ ["ver:\"#{haystack_project.haystack_version}\" id:@#{self.haystack_point_id}",'ts,val'] + data.map{ |d| "#{d[:time]},#{d[:value]}"}
38
+
39
+ res = connection.post('hisWrite') do |req|
40
+ req.headers['Content-Type'] = 'text/plain'
41
+ req.body = query.join("\n")
42
+ end
43
+
44
+ JSON.parse(res.body)['meta']['ok'].present?
45
+ end
46
+
47
+ def data(start, finish = nil, as_datetime = false) # as_datetime currently ignored
48
+ return unless haystack_valid? #may choose to throw exception instead
49
+
50
+ range = [start]
51
+ range << finish unless finish.nil?
52
+ # clean up the range argument before passing through to hisRead
53
+ # ----------------
54
+ r = HaystackRuby::Range.new(range, self.haystack_time_zone)
55
+
56
+ res = his_read r.to_s
57
+ reformat_timeseries(res['rows'], as_datetime)
58
+ end
59
+
60
+ def write_data(data)
61
+ # format data for his_write
62
+ data = data.map do |d|
63
+ {
64
+ time: HaystackRuby::Timestamp.convert_to_string(d[:time], self.haystack_time_zone),
65
+ value: d[:value]
66
+ }
67
+ end
68
+ his_write data
69
+ end
70
+
71
+ # map from
72
+ def reformat_timeseries data, as_datetime
73
+ data.map do |d|
74
+ time = (as_datetime) ? DateTime.parse(d['ts']) : DateTime.parse(d['ts']).to_i
75
+ val = HaystackRuby::Object.new(d['val'])
76
+ {:time => time, :value => val.value}
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,159 @@
1
+ module HaystackRuby
2
+ require 'json'
3
+ require 'pp'
4
+ # may consider making this a mixin instead
5
+ class Project
6
+
7
+ attr_accessor :name, :haystack_version, :url #required
8
+ def initialize(name, config)
9
+ @name = name
10
+ @url = (config['secure']) ? 'https://' : 'http://'
11
+ @url = "#{@url}#{config['base_url']}"
12
+ @haystack_version = config['haystack_version']
13
+ # expect to use basic auth
14
+ if config['credentials'].present?
15
+ @credentials = config['credentials']
16
+ #for now at least, we fake the user object
17
+ #expect to use scram
18
+ else
19
+ user = OpenStruct.new
20
+ user.username = config['username']
21
+ user.password = config['password']
22
+ # @creds_path = config['credentials_path']
23
+ # creds = YAML.load File.new(@creds_path).read
24
+ # user.username = creds['username']
25
+ # user.password = creds['password']
26
+ authorize user
27
+ end
28
+ end
29
+
30
+ def authorize user
31
+ auth_conv = HaystackRuby::Auth::Scram::Conversation.new(user, @url)
32
+ auth_conv.authorize
33
+ @auth_token = auth_conv.auth_token
34
+ raise HaystackRuby::Error, "scram authorization failed" unless @auth_token.present?
35
+ end
36
+
37
+
38
+ # for now, setting up to have a single connection per project
39
+ def connection
40
+ # if @credentials.nil? && @auth_token.nil?
41
+ # authorize #will either set auth token or raise error
42
+ # end
43
+ @connection ||= Faraday.new(:url => @url) do |faraday|
44
+ faraday.request :url_encoded # form-encode POST params
45
+ faraday.response :logger # log requests to STDOUT
46
+ faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
47
+ faraday.headers['Authorization'] = @auth_token.present? ? "BEARER authToken=#{@auth_token}" : "Basic #@credentials"
48
+ faraday.headers['Accept'] = 'application/json' #TODO enable more formats
49
+ end
50
+ end
51
+
52
+ def read(params)
53
+ body = ["ver:\"#{@haystack_version}\""]
54
+ body << params.keys.join(',')
55
+ body << params.values.join(',')
56
+ res = self.connection.post('read') do |req|
57
+ req.headers['Content-Type'] = 'text/plain'
58
+ req.body = body.join("\n")
59
+ end
60
+ JSON.parse! res.body
61
+ end
62
+
63
+ # return meta data for all equip with related points
64
+ def equip_point_meta
65
+ # begin
66
+ equips = read({filter: '"equip"'})['rows']
67
+ puts equips
68
+ equips.map! do |eq|
69
+ eq.delete('disMacro')
70
+ eq['description'] = eq['id'].match(/[(NWTC)|(\$siteRef)] (.*)/)[1]
71
+ eq['id'] = eq['id'].match(/:([a-z0-9\-]*)/)[1]
72
+ eq['points'] = []
73
+ read({filter: "\"point and equipRef==#{eq['id']}\""})['rows'].each do |p|
74
+ p.delete('analytics')
75
+ p.delete('disMacro')
76
+ p.delete('csvUnit')
77
+ p.delete('csvColumn')
78
+ p.delete('equipRef')
79
+ p.delete('point')
80
+ p.delete('siteRef')
81
+
82
+ p['id'] = p['id'].match(/:([a-z0-9\-]*)/)[1]
83
+ p['name'] = p['navName']
84
+ p.delete('navName')
85
+ eq['points'] << p
86
+ end
87
+ eq
88
+ end
89
+ # rescue Exception => e
90
+ puts "error: #{e}"
91
+ nil
92
+ # end
93
+ end
94
+
95
+ def ops
96
+ JSON.parse!(self.connection.get("ops").body)['rows']
97
+ end
98
+
99
+ def valid?
100
+ !(@name.nil? || @haystack_version.nil? || @url.nil?)
101
+ end
102
+
103
+ # http://www.skyfoundry.com/doc/docSkySpark/Ops#commit
104
+ # grid is array of strings
105
+ def commit grid
106
+ puts 'grid = '
107
+ pp grid.join "\n"
108
+ res = self.connection.post('commit') do |req|
109
+ req.headers['Content-Type'] = 'text/plain'
110
+ req.body = grid.join "\n"
111
+ end
112
+ JSON.parse! res.body
113
+ end
114
+
115
+ # params is array of hashes: {name: xx, type: xx, value: xx}
116
+ def add_rec params
117
+ grid = ["ver:\"#{@haystack_version}\" commit:\"add\""]
118
+ grid << params.map{|p| p[:name]}.join(',')
119
+ values = params.map do |p|
120
+ p[:value] = "\"#{p[:value]}\"" if p[:type] == 'String'
121
+ p[:value]
122
+ end
123
+ grid << values.join(',')
124
+ res = commit grid
125
+ # return id of new rec
126
+ res['rows'][0]['id']
127
+ end
128
+
129
+ # TODO fix these. weird sensitivities around mod timestamp (format and time)
130
+ # params is array of hashes: {name: xx, type: xx, value: xx}
131
+ # def update_rec id,params
132
+ # grid = ["ver:\"#{@haystack_version}\" commit:\"update\""]
133
+ # grid << 'id,mod,' + params.map{|p| p[:name]}.join(',')
134
+ # values = params.map do |p|
135
+ # p[:value] = "\"#{p[:value]}\"" if p[:type] == 'String'
136
+ # p[:value]
137
+ # end
138
+ # grid << "#{id},2017-01-09T17:21:31.197Z UTC,#{values.join(',')}"
139
+ # puts "dumping grid #{grid}"
140
+ #
141
+ # commit grid
142
+ # end
143
+
144
+ def remove_rec id
145
+ grid = ["ver:\"#{@haystack_version}\" commit:\"remove\""]
146
+ grid << 'id,mod'
147
+ grid << "#{id},#{DateTime.now}"
148
+ commit grid
149
+ end
150
+ private
151
+
152
+ # def persist_creds
153
+ # creds = {username: @username, password: @password, auth_token: @auth_token}
154
+ # File.open(@creds_path,'w') do |h|
155
+ # h.write creds.to_yaml
156
+ # end
157
+ # end
158
+ end
159
+ end
@@ -0,0 +1,18 @@
1
+ require 'rails'
2
+ module Rails
3
+ module HaystackRuby
4
+ class Railtie < Rails::Railtie
5
+ initializer "haystack_ruby.load-config" do
6
+ config_file = Rails.root.join("config", "haystack_ruby.yml")
7
+ if config_file.file?
8
+ begin
9
+ ::HaystackRuby::Config.load!(config_file)
10
+ rescue Exception => e
11
+ # TODO better error handling
12
+ puts "Error loading config for haystack_ruby: #{e}"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,34 @@
1
+ # from haystack docs:
2
+ #
3
+ # Ranges are exclusive of start timestamp and inclusive of end timestamp. The {date} and {dateTime} options must be correctly Zinc encoded. DateTime based ranges must be in the same timezone of the entity (timezone conversion is explicitly disallowed). Date based ranges are always inferred to be from midnight of starting date to midnight of the day after ending date using the timezone of the his entity being queried.
4
+ # ----------------
5
+
6
+
7
+ module HaystackRuby
8
+ class Range
9
+ def initialize(range,time_zone)
10
+ @haystack_time_zone = time_zone
11
+ @finish = nil
12
+ if range.kind_of? Array
13
+ raise HaystackRuby::Error, 'Too many values for range' if range.count > 2
14
+ @start = Timestamp.convert_to_string(range.first, time_zone)
15
+ if range.count > 1
16
+ @finish = Timestamp.convert_to_string(range.last, time_zone)
17
+ raise HaystackRuby::Error, 'Start must be before End' if DateTime.parse(@start) >= DateTime.parse(@finish)
18
+ end
19
+ else
20
+ @start = Timestamp.convert_to_string(range, time_zone)
21
+ end
22
+ end
23
+
24
+ def to_s
25
+ if @finish
26
+ "#{@start},#{@finish}"
27
+ elsif @start
28
+ "#{@start}"
29
+ else
30
+ ''
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,20 @@
1
+ module HaystackRuby
2
+ class Timestamp
3
+ # convert ts to a Project Haystack compliant string.
4
+ # ts is a timestamp, tz is a Haystack timezone string
5
+ # for now assumes application timezone is set correctly
6
+ # TODO add a mapping between Ruby and Haystack timezone strings and use here
7
+ def self.convert_to_string ts, tz
8
+ if ts.kind_of? DateTime
9
+ t = ts.to_s
10
+ elsif ts.kind_of? Integer
11
+ t = Time.at(ts).to_datetime.to_s
12
+ elsif ts.kind_of? Date
13
+ t = ts.to_datetime.to_s
14
+ else
15
+ raise HaystackRuby::Error, "param must be one of Date, DateTime, Integer"
16
+ end
17
+ "#{t} #{tz}"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ module HaystackRuby
2
+ module Types
3
+ module Bin
4
+ def set_fields str_value
5
+ @haystack_type = 'Bin'
6
+ match = /\Ab:(.*)\z/.match str_value
7
+ raise HaystackRuby::Error, "invalid HaystackRuby::Types::Bin: #{str_value}" if match.nil?
8
+ # Value is mime type
9
+ @value = match[1]
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ module HaystackRuby
2
+ module Types
3
+ module Coord
4
+ def set_fields str_value
5
+ @haystack_type = 'Coord'
6
+ match = /\Ac:([0-9.-]*),([0-9.-]*)\z/.match str_value
7
+ begin
8
+ @value = [match[1].to_f, match[2].to_f] #value is array of [lat, lng]
9
+ rescue Exception=>e
10
+ raise HaystackRuby::Error, "invalid HaystackRuby::Types::Coord #{str_value}. Error #{e}"
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module HaystackRuby
2
+ module Types
3
+ module Date
4
+ def set_fields str_value
5
+ @haystack_type = 'Date'
6
+ match = /\Ad:([-0-9]*)\z/.match str_value
7
+ begin
8
+ @value = Date.parse match[1]
9
+ rescue Exception=>e
10
+ raise HaystackRuby::Error, "invalid HaystackRuby::Types::Date #{str_value}. Error #{e}"
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module HaystackRuby
2
+ module Types
3
+ module DateTime
4
+ def set_fields str_value
5
+ @haystack_type = 'DateTime'
6
+ match = /\At:(.*)\z/.match str_value
7
+ begin
8
+ @value = DateTime.parse match[1]
9
+ rescue Exception=>e
10
+ raise HaystackRuby::Error, "invalid HaystackRuby::Types::DateTime #{str_value}. Error #{e}"
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ module HaystackRuby
2
+ module Types
3
+ module Marker
4
+ def set_fields str_value
5
+ @haystack_type = 'Marker'
6
+ @value = true
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module HaystackRuby
2
+ module Types
3
+ module NA
4
+ def set_fields str_value
5
+ @haystack_type = 'NA'
6
+ @value = 'NA'
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,14 @@
1
+ module HaystackRuby
2
+ module Types
3
+ module Number
4
+ def set_fields str_value
5
+ @haystack_type = 'Number'
6
+ match = /\An:([-0-9\.]*)( .*)*\z/.match str_value
7
+ raise HaystackRuby::Error, "invalid HaystackRuby::Types::Number: #{str_value}" if match.nil?
8
+ @value = match[1].to_f
9
+ # also set unit if available
10
+ @unit = match[2].strip if match[2].present?
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module HaystackRuby
2
+ module Types
3
+ module Ref
4
+ def set_fields str_value
5
+ @haystack_type = 'Ref'
6
+ match = /\Ar:([^ ]*) (.*)\z/.match str_value
7
+ raise HaystackRuby::Error, HaystackRuby::Error, "invalid HaystackRuby::Types::Ref: #{str_value}" if match.nil?
8
+ @value = match[1] #ref id
9
+ @description = match[2]
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ module HaystackRuby
2
+ module Types
3
+ module Str
4
+ def set_fields str_value
5
+ @haystack_type = 'Str'
6
+ match = /\As:(.*)\z/.match str_value
7
+ raise "invalid HaystackRuby::Types::Str: #{str_value}" if match.nil?
8
+ @value = match[1]
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ module HaystackRuby
2
+ module Types
3
+ # Since there is no Ruby Hour class, value is a string "hr:minute"
4
+ module Time
5
+ def set_fields str_value
6
+ @haystack_type = 'Time'
7
+ match = /\Ah:([0-9:]*)\z/.match str_value
8
+ begin
9
+ @value = match[1]
10
+ rescue Exception=>e
11
+ raise HaystackRuby::Error, "invalid HaystackRuby::Types::Time #{str_value}. Error #{e}"
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,12 @@
1
+ module HaystackRuby
2
+ module Types
3
+ module Uri
4
+ def set_fields str_value
5
+ @haystack_type = 'Uri'
6
+ match = /\Au:(.*)\z/.match str_value
7
+ raise HaystackRuby::Error, "invalid HaystackRuby::Types::Uri: #{str_value}" if match.nil?
8
+ @value = match[1]
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ require 'faraday'
2
+ require 'haystack_ruby/auth/conversation'
3
+ require 'haystack_ruby/config'
4
+ require 'haystack_ruby/error'
5
+ require 'haystack_ruby/object'
6
+ require 'haystack_ruby/point'
7
+ require 'haystack_ruby/project'
8
+ require 'haystack_ruby/range'
9
+ require 'haystack_ruby/timestamp'
10
+
11
+ require 'active_support/core_ext/hash'
12
+ if defined?(Rails)
13
+ require "haystack_ruby/railtie"
14
+ end
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+
3
+ describe HaystackRuby::Auth::Scram::Conversation do
4
+ describe 'connection' do
5
+
6
+ # it 'receives authorization challenge response to hello' do
7
+ # expect(conv.hello.status.should == 401 )
8
+ # end
9
+ before :each do
10
+ user = OpenStruct.new
11
+ user.username = CONF[PROJECT]['username']
12
+ user.password = CONF[PROJECT]['password']
13
+ url = HaystackRuby::Config.projects[PROJECT].url
14
+ @conv = HaystackRuby::Auth::Scram::Conversation.new(user, url)
15
+ end
16
+ it 'receives auth challenge response to first message' do
17
+ expect(@conv.send_first_message.status).to be == 401
18
+ end
19
+ it 'sends valid rnonce in continue message' do
20
+ res = @conv.send_first_message
21
+ @conv.parse_first_response(res)
22
+ puts "nonce #{@conv.nonce}, servernonce #{@conv.server_nonce}"
23
+ expect(@conv.nonce.length).to be > 0
24
+ expect(@conv.server_nonce.index(@conv.nonce)).to be == 0
25
+ end
26
+ it 'server responds to second client message with authorization' do
27
+ res = @conv.send_first_message
28
+ @conv.parse_first_response(res)
29
+ expect(@conv.send_second_message.status).to be == 200
30
+ end
31
+ it 'should have nonempty auth token after full authorization' do
32
+ @conv.authorize
33
+ expect(@conv.auth_token).not_to be_empty
34
+ end
35
+ it 'should load page using auth token' do
36
+ @conv.authorize
37
+ expect(@conv.test_auth_token.status).to be == 200
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+ describe HaystackRuby::Config do
3
+ describe '.projects' do
4
+ context 'good config file' do
5
+ before do
6
+ @projects = HaystackRuby::Config.projects
7
+ @demo = @projects[PROJECT]
8
+ end
9
+ it 'returns a project' do
10
+ expect(@demo).to be_a_kind_of HaystackRuby::Project
11
+ end
12
+ end
13
+ context 'bad project in config file' do
14
+ end
15
+ context 'missing config file' do
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,144 @@
1
+ require 'spec_helper'
2
+ require 'models/point_demo'
3
+ describe HaystackRuby::Point do
4
+ before do
5
+ @point = PointDemo.new(PT_PROJ,PT_ID,PT_TZ)
6
+ end
7
+ describe '#haystack_project' do
8
+ context 'real project name' do
9
+ it 'responds to method' do
10
+ expect(@point).to respond_to :haystack_project
11
+ end
12
+ it 'returns a project' do
13
+ expect(@point.haystack_project).to be_a_kind_of HaystackRuby::Project
14
+ end
15
+ end
16
+ end
17
+ describe '#his_read' do
18
+ context 'valid id and range' do
19
+ before do
20
+ @res = @point.his_read('yesterday')
21
+ end
22
+ it 'returns 2 columns' do
23
+ expect(@res['cols'].count).to eq 2
24
+ end
25
+ it 'returns array of rows' do
26
+ expect(@res['rows']).to be_a_kind_of Array
27
+ end
28
+ end
29
+ end
30
+ describe '#his_write' do
31
+ context 'valid data' do
32
+ before do
33
+ data = [{time: "#{DateTime.now.to_s} #{@point.haystack_time_zone}", value: rand(-2000..2000).to_f / 10}, {time: "#{(DateTime.now - 1.day).to_s} #{@point.haystack_time_zone}", value: rand(-2000..2000).to_f / 10}]
34
+ @res = @point.his_write(data)
35
+ end
36
+ it 'returns true' do
37
+ expect(@res).to eq true
38
+ end
39
+ end
40
+ end
41
+ describe '#write_data' do
42
+ context 'valid data' do
43
+ before do
44
+ data = [{time: DateTime.now, value: -1}]
45
+ @res = @point.write_data(data)
46
+ end
47
+ it 'returns true' do
48
+ expect(@res).to eq true
49
+ end
50
+ end
51
+ end
52
+
53
+ describe '#metadata' do
54
+ context 'valid id' do
55
+ it 'returns metadata for the point' do
56
+
57
+ expect(@point.meta_data).to be_a_kind_of Hash
58
+ end
59
+ end
60
+ end
61
+
62
+ describe '#data' do
63
+ context 'valid id and range' do
64
+ before do
65
+ @data = @point.data(Date.today.prev_day)
66
+ @d = @data.first
67
+ end
68
+ it 'returns data with expected format' do
69
+ expect(@d[:time]).to_not be_nil
70
+ expect(@d[:value]).to_not be_nil
71
+ end
72
+ it 'returns time as epoch' do
73
+ expect(@d[:time]).to be_a_kind_of Integer
74
+ end
75
+ end
76
+ context 'as_datetime true' do
77
+ before do
78
+ @d = @point.data(Date.today.prev_day, nil, true).first
79
+ end
80
+ it 'returns time in DateTime format' do
81
+ expect(@d[:time]).to be_kind_of DateTime
82
+ end
83
+ end
84
+ context 'various range formats' do
85
+ context 'Dates' do
86
+ it 'returns data when two ascending Date values' do
87
+ data = @point.data(Date.today.prev_day, Date.today)
88
+ expect(data.count).to be > 0
89
+ end
90
+ it 'throws error start is after finish' do
91
+ expect {
92
+ data = @point.data(Date.today, Date.today.prev_day)
93
+ }.to raise_error HaystackRuby::Error
94
+ end
95
+ it 'returns data when no finish' do
96
+ data = @point.data(Date.today.prev_day)
97
+ expect(data.count).to be > 0
98
+ end
99
+ end
100
+ context 'Integers' do
101
+ it 'returns data when two ascending Integer values' do
102
+ data = @point.data(Date.today.prev_day.to_time.to_i, Date.today.to_time.to_i)
103
+ expect(data.count).to be > 0
104
+ end
105
+ it 'throws error start is after finish' do
106
+ expect {
107
+ data = @point.data(Date.today.to_time.to_i, Date.today.prev_day.to_time.to_i)
108
+ }.to raise_error HaystackRuby::Error
109
+ end
110
+ it 'returns data when no finish' do
111
+ data = @point.data(Date.today.prev_day.to_time.to_i)
112
+ expect(data.count).to be > 0
113
+ end
114
+ end
115
+ context 'DateTimes' do
116
+ it 'returns data when two ascending DateTime values' do
117
+ data = @point.data(Date.today.prev_day.to_datetime, Date.today.to_datetime)
118
+ expect(data.count).to be > 0
119
+ end
120
+ it 'throws error start is after finish' do
121
+ expect {
122
+ @point.data(Date.today.to_datetime, Date.today.prev_day.to_datetime)
123
+ }.to raise_error HaystackRuby::Error
124
+
125
+ end
126
+ it 'returns data when one value' do
127
+ data = @point.data(Date.today.prev_day.to_datetime)
128
+ expect(data.count).to be > 0
129
+ end
130
+ end
131
+ it 'returns data when range is Date' do
132
+ data = @point.data(Date.today.prev_day)
133
+ expect(data.count).to be > 0
134
+ end
135
+ it 'does not accept string values for range' do
136
+ expect { data = @point.data(Date.today.prev_day.to_s) }.to raise_error HaystackRuby::Error
137
+ end
138
+ it 'returns data when range is start DateTime' do
139
+ data = @point.data(Date.today.prev_day.to_datetime)
140
+ expect(data.count).to be > 0
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+ describe HaystackRuby::Project do
3
+ before do
4
+ @demo = HaystackRuby::Config.projects[PT_PROJ]
5
+ end
6
+ describe '#connection' do
7
+ context 'defined project name' do
8
+ it 'returns valid Faraday connection' do
9
+ expect(@demo.connection).to be_a_kind_of Faraday::Connection
10
+ end
11
+ it 'sets Authorization header' do
12
+ expect(@demo.connection.headers['Authorization']).to_not be_nil
13
+ end
14
+ it 'accepts json' do
15
+ expect(@demo.connection.headers['Accept']).to eq 'application/json'
16
+ end
17
+ end
18
+ context 'undefined project name' do
19
+ end
20
+ context 'missing config file' do
21
+ end
22
+ end
23
+ describe '#read' do
24
+ context 'simple filter' do
25
+ it 'returns cols' do
26
+ expect(@demo.read({:filter => '"site"'})['cols']).to be_a_kind_of Array
27
+ end
28
+ it 'returns rows' do
29
+ expect(@demo.read({:filter => '"site"'})['rows']).to be_a_kind_of Array
30
+ end
31
+ end
32
+ end
33
+ describe '#add_rec' do
34
+ context 'point' do
35
+ it 'returns id' do
36
+ params = [{name: 'sp',type: 'Marker',value: 'M'},{name: 'dis',type: 'String',value: 'Test'},{name: 'point', type: 'Marker', value: 'M'},{name: 'equipRef',type: 'Ref',value: '@1d56759c-8f9214b6'},{name: 'siteRef', type: 'Ref', value: '@1d56758c-c15f5708'}]
37
+ res = @demo.add_rec(params )
38
+ expect(res).to be_a_kind_of String
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,10 @@
1
+ class PointDemo
2
+ # these fields are required on the class point is mixed into
3
+ attr_accessor :haystack_project_name, :haystack_point_id, :haystack_time_zone
4
+ include HaystackRuby::Point
5
+ def initialize(proj_name, point_id, tz)
6
+ @haystack_project_name = proj_name
7
+ @haystack_point_id = point_id
8
+ @haystack_time_zone = tz
9
+ end
10
+ end
@@ -0,0 +1,22 @@
1
+ require 'bundler/setup'
2
+ require 'yaml'
3
+ Bundler.setup
4
+ require 'haystack_ruby' # and any other gems you need
5
+
6
+ # configuration constants used for testing
7
+ CONFIG_PATH = 'config/example.yml' #path to the settings file, relative to directory root
8
+ CONFIG_ENV = 'test' #environment to load from settings file for testing
9
+ PROJECT = 'demov3' #name of project to use for testing. Must match name in the settings file.
10
+ PT_ID = '1d5675a8-867de4b8' #Haystack ID of an existing point that is safe for to use for testing. NOTE that this point will be written to.
11
+ PT_PROJ = PROJECT # Optionally use a different project name for the point tests
12
+ PT_TZ = 'Denver' # Timezone of your test point
13
+
14
+
15
+ Time.zone='Mountain Time (US & Canada)'
16
+ RSpec.configure do |config|
17
+ config.before(:suite) do
18
+ CONF = YAML.load(File.new(CONFIG_PATH).read).with_indifferent_access[CONFIG_ENV]
19
+ HaystackRuby::Config.load_configuration CONF
20
+ end
21
+
22
+ end
metadata ADDED
@@ -0,0 +1,132 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: haystack_ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Anya Petersen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-03-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: openssl
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Ruby adapter for Project Haystack REST API
70
+ email: anya.petersen@nrel.gov
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - lib/haystack_ruby.rb
76
+ - lib/haystack_ruby/auth/conversation.rb
77
+ - lib/haystack_ruby/config.rb
78
+ - lib/haystack_ruby/error.rb
79
+ - lib/haystack_ruby/object.rb
80
+ - lib/haystack_ruby/point.rb
81
+ - lib/haystack_ruby/project.rb
82
+ - lib/haystack_ruby/railtie.rb
83
+ - lib/haystack_ruby/range.rb
84
+ - lib/haystack_ruby/timestamp.rb
85
+ - lib/haystack_ruby/types/bin.rb
86
+ - lib/haystack_ruby/types/coord.rb
87
+ - lib/haystack_ruby/types/date.rb
88
+ - lib/haystack_ruby/types/date_time.rb
89
+ - lib/haystack_ruby/types/marker.rb
90
+ - lib/haystack_ruby/types/n_a.rb
91
+ - lib/haystack_ruby/types/number.rb
92
+ - lib/haystack_ruby/types/ref.rb
93
+ - lib/haystack_ruby/types/str.rb
94
+ - lib/haystack_ruby/types/time.rb
95
+ - lib/haystack_ruby/types/uri.rb
96
+ - spec/haystack_ruby/auth/scram_spec.rb
97
+ - spec/haystack_ruby/config_spec.rb
98
+ - spec/haystack_ruby/point_spec.rb
99
+ - spec/haystack_ruby/project_spec.rb
100
+ - spec/models/point_demo.rb
101
+ - spec/spec_helper.rb
102
+ homepage: https://github.com/NREL/haystack_ruby
103
+ licenses:
104
+ - MIT
105
+ metadata: {}
106
+ post_install_message:
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ requirements: []
121
+ rubyforge_project:
122
+ rubygems_version: 2.6.11
123
+ signing_key:
124
+ specification_version: 4
125
+ summary: Project Haystack Ruby Adapter
126
+ test_files:
127
+ - spec/haystack_ruby/auth/scram_spec.rb
128
+ - spec/haystack_ruby/config_spec.rb
129
+ - spec/haystack_ruby/point_spec.rb
130
+ - spec/haystack_ruby/project_spec.rb
131
+ - spec/models/point_demo.rb
132
+ - spec/spec_helper.rb