noms-command 0.5.0
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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/Gemfile +4 -0
- data/LICENSE +191 -0
- data/README.rst +376 -0
- data/ROADMAP.rst +127 -0
- data/Rakefile +49 -0
- data/TODO.rst +7 -0
- data/bin/noms2 +20 -0
- data/fixture/dnc.rb +120 -0
- data/fixture/identity +5 -0
- data/fixture/public/dnc.json +22 -0
- data/fixture/public/echo.json +7 -0
- data/fixture/public/files/data.json +12 -0
- data/fixture/public/files/foo.json +2 -0
- data/fixture/public/lib/dnc.js +81 -0
- data/fixture/public/lib/noms-args.js +13 -0
- data/fixture/public/lib/showopt.js +18 -0
- data/fixture/public/location.json +8 -0
- data/fixture/public/showopt.json +15 -0
- data/fixture/rig2json +21 -0
- data/lib/noms/command/application.rb +204 -0
- data/lib/noms/command/auth/identity.rb +62 -0
- data/lib/noms/command/auth.rb +117 -0
- data/lib/noms/command/base.rb +22 -0
- data/lib/noms/command/document.rb +59 -0
- data/lib/noms/command/error.rb +11 -0
- data/lib/noms/command/formatter.rb +178 -0
- data/lib/noms/command/urinion/data.rb +63 -0
- data/lib/noms/command/urinion.rb +29 -0
- data/lib/noms/command/useragent.rb +134 -0
- data/lib/noms/command/version.rb +7 -0
- data/lib/noms/command/window.rb +95 -0
- data/lib/noms/command/xmlhttprequest.rb +181 -0
- data/lib/noms/command.rb +107 -0
- data/noms-command.gemspec +30 -0
- data/spec/01noms-command_spec.rb +30 -0
- data/spec/02noms-command.sh +31 -0
- data/spec/03application_spec.rb +47 -0
- data/spec/04application_spec.rb +61 -0
- data/spec/05formatter_spec.rb +195 -0
- data/spec/06urinion_data.rb +20 -0
- data/spec/07js_spec.rb +87 -0
- data/spec/08xhr_spec.rb +209 -0
- data/spec/09bookmarks_spec.rb +60 -0
- data/spec/10auth_spec.rb +33 -0
- data/spec/spec_helper.rb +40 -0
- metadata +228 -0
@@ -0,0 +1,204 @@
|
|
1
|
+
#!ruby
|
2
|
+
|
3
|
+
require 'noms/command/version'
|
4
|
+
|
5
|
+
require 'mime-types'
|
6
|
+
require 'v8'
|
7
|
+
|
8
|
+
require 'noms/command/window'
|
9
|
+
require 'noms/command/useragent'
|
10
|
+
require 'noms/command/error'
|
11
|
+
require 'noms/command/urinion'
|
12
|
+
require 'noms/command/formatter'
|
13
|
+
require 'noms/command/document'
|
14
|
+
require 'noms/command/xmlhttprequest'
|
15
|
+
require 'noms/command/base'
|
16
|
+
|
17
|
+
class NOMS
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
class NOMS::Command
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
class NOMS::Command::Application < NOMS::Command::Base
|
26
|
+
|
27
|
+
# Should user-agent actually be here?
|
28
|
+
attr_accessor :window, :options,
|
29
|
+
:type, :body, :useragent,
|
30
|
+
:document
|
31
|
+
|
32
|
+
def initialize(origin, argv, attrs={})
|
33
|
+
@document = nil
|
34
|
+
@origin = NOMS::Command::URInion.parse(origin)
|
35
|
+
if @origin.scheme == 'file' and @origin.host.nil?
|
36
|
+
@origin.host = 'localhost'
|
37
|
+
end
|
38
|
+
@argv = argv
|
39
|
+
@options = { }
|
40
|
+
@type = nil
|
41
|
+
|
42
|
+
@log = attrs[:logger] || default_logger
|
43
|
+
|
44
|
+
@window = NOMS::Command::Window.new($0, @origin, :logger => @log)
|
45
|
+
|
46
|
+
@log.debug "Application #{argv[0]} has origin: #{origin}"
|
47
|
+
@useragent = NOMS::Command::UserAgent.new(@origin, :logger => @log,
|
48
|
+
:specified_identities => (attrs[:specified_identities] || []))
|
49
|
+
end
|
50
|
+
|
51
|
+
def fetch!
|
52
|
+
# Get content and build object, set @type
|
53
|
+
case @origin.scheme
|
54
|
+
when 'file'
|
55
|
+
@type = (MIME::Types.of(@origin.path).first || MIME::Types['text/plain'].first).content_type
|
56
|
+
@body = File.open(@origin.path, 'r') { |fh| fh.read }
|
57
|
+
when 'data'
|
58
|
+
@type = @origin.mime_type
|
59
|
+
raise NOMS::Command::Error.new("data URLs must contain application/json") unless @type == 'application/json'
|
60
|
+
@body = @origin.data
|
61
|
+
when /^http/
|
62
|
+
response, landing_url = @useragent.get(@origin)
|
63
|
+
new_url = landing_url
|
64
|
+
@origin = new_url
|
65
|
+
@useragent.origin = new_url
|
66
|
+
@window.origin = new_url
|
67
|
+
@log.debug "Setting origin to: #{@origin}"
|
68
|
+
if response.ok?
|
69
|
+
# Unlike typical ReST data sources, this
|
70
|
+
# should very rarely fail unless there is
|
71
|
+
# a legitimate communication issue.
|
72
|
+
@type = response.contenttype || 'text/plain'
|
73
|
+
@body = response.content
|
74
|
+
else
|
75
|
+
raise NOMS::Command::Error.new("Failed to request #{@origin}: #{response.status} #{response.reason}")
|
76
|
+
end
|
77
|
+
else
|
78
|
+
raise NOMS::Command::Error.new("noms command #{@argv[0].inspect} not found: not a URL or bookmark")
|
79
|
+
end
|
80
|
+
|
81
|
+
case @type
|
82
|
+
when /^(application|text)\/(x-|)json/
|
83
|
+
begin
|
84
|
+
@body = JSON.parse(@body)
|
85
|
+
rescue JSON::ParserError => e
|
86
|
+
raise NOMS::Command::Error.new("JSON error in #{@origin}: #{e.message}")
|
87
|
+
end
|
88
|
+
if @body.respond_to? :has_key? and @body.has_key? '$doctype'
|
89
|
+
@type = @body['$doctype']
|
90
|
+
@log.debug "Treating as #{@type} document"
|
91
|
+
@document = NOMS::Command::Document.new @body
|
92
|
+
@document.argv = @argv
|
93
|
+
@document.exitcode = 0
|
94
|
+
else
|
95
|
+
@log.debug "Treating as raw object (no '$doctype')"
|
96
|
+
@type = 'noms-raw'
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def exitcode
|
102
|
+
@document ? @document.exitcode : 0
|
103
|
+
end
|
104
|
+
|
105
|
+
def render!
|
106
|
+
if @document and @document.script
|
107
|
+
# Crashes when using @window as global object
|
108
|
+
@v8 = V8::Context.new
|
109
|
+
# Set up same-origin context and stuff--need
|
110
|
+
# Ruby objects to do XHR and limit local I/O
|
111
|
+
@window.document = @document
|
112
|
+
@v8[:window] = @window
|
113
|
+
@v8[:document] = @document
|
114
|
+
@v8.eval 'var alert = function (s) { window.alert(s); };'
|
115
|
+
@v8.eval 'var prompt = function (s, echo) { window.prompt(s, echo); };'
|
116
|
+
@v8.eval 'var location = window.location;'
|
117
|
+
@v8.eval 'var console = window.console;'
|
118
|
+
NOMS::Command::XMLHttpRequest.origin = @origin
|
119
|
+
NOMS::Command::XMLHttpRequest.useragent = @useragent
|
120
|
+
@v8[:XMLHttpRequest] = NOMS::Command::XMLHttpRequest
|
121
|
+
script_index = 0
|
122
|
+
@document.script.each do |script|
|
123
|
+
if script.respond_to? :has_key? and script.has_key? '$source'
|
124
|
+
# Parse relative URL and load
|
125
|
+
response, landing_url = @useragent.get(script['$source'])
|
126
|
+
# Don't need landing_url
|
127
|
+
script_name = File.basename(@useragent.absolute_url(script['$source']).path)
|
128
|
+
script_ref = "#{script_index},#{script_name}"
|
129
|
+
if response.ok?
|
130
|
+
case response.contenttype
|
131
|
+
when /^(application|text)\/(x-|)javascript/
|
132
|
+
begin
|
133
|
+
@v8.eval response.content
|
134
|
+
rescue StandardError => e
|
135
|
+
@log.warn "Javascript[#{script_ref}] error: #{e.message}"
|
136
|
+
@log.debug e.backtrace.join("\n")
|
137
|
+
end
|
138
|
+
else
|
139
|
+
@log.warn "Unsupported script type '#{response.contenttype.inspect}' " +
|
140
|
+
"for script from #{script['$source'].inspect}"
|
141
|
+
end
|
142
|
+
else
|
143
|
+
@log.warn "Couldn't load script from #{script['$source'].inspect}: #{response.status} #{response.reason}"
|
144
|
+
@log.debug "Body of unsuccessful request: #{response.body}"
|
145
|
+
end
|
146
|
+
else
|
147
|
+
# It's javascript text
|
148
|
+
script_ref = "#{script_index},\"#{abbrev(script)}\""
|
149
|
+
begin
|
150
|
+
@v8.eval script
|
151
|
+
rescue StandardError => e
|
152
|
+
@log.warn "Javascript[#{script_ref}] error: #{e.message}"
|
153
|
+
@log.debug e.backtrace.join("\n")
|
154
|
+
end
|
155
|
+
end
|
156
|
+
script_index += 1
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def abbrev(s, limit=10)
|
162
|
+
if s.length > (limit - 3)
|
163
|
+
s[0 .. (limit - 3)] + '...'
|
164
|
+
else
|
165
|
+
s
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def display
|
170
|
+
case @type
|
171
|
+
when 'noms-v2'
|
172
|
+
NOMS::Command::Formatter.new(_sanitize(@document.body)).render
|
173
|
+
when 'noms-raw'
|
174
|
+
@body.to_yaml
|
175
|
+
when /^text(\/|$)/
|
176
|
+
@body
|
177
|
+
else
|
178
|
+
if @window.isatty
|
179
|
+
# Should this be here?
|
180
|
+
@log.warn "Unknown data of type '#{@type}' not sent to terminal"
|
181
|
+
[]
|
182
|
+
else
|
183
|
+
@body
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Get rid of V8 stuff
|
189
|
+
def _sanitize(thing)
|
190
|
+
if thing.kind_of? V8::Array or thing.respond_to? :to_ary
|
191
|
+
thing.map do |item|
|
192
|
+
_sanitize item
|
193
|
+
end
|
194
|
+
elsif thing.respond_to? :keys
|
195
|
+
Hash[
|
196
|
+
thing.keys.map do |key|
|
197
|
+
[key, _sanitize(thing[key])]
|
198
|
+
end]
|
199
|
+
else
|
200
|
+
thing
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
#!ruby
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
class NOMS
|
6
|
+
|
7
|
+
end
|
8
|
+
|
9
|
+
class NOMS::Command
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
class NOMS::Command::Auth < NOMS::Command::Base
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
class NOMS::Command::Auth::Identity < NOMS::Command::Base
|
18
|
+
include Enumerable
|
19
|
+
|
20
|
+
def initialize(auth, h, attrs={})
|
21
|
+
@log = attrs[:logger] || default_logger
|
22
|
+
@auth = auth
|
23
|
+
@data = h
|
24
|
+
end
|
25
|
+
|
26
|
+
def [](key)
|
27
|
+
@data[key]
|
28
|
+
end
|
29
|
+
|
30
|
+
def []=(key, value)
|
31
|
+
@data[key] = value
|
32
|
+
end
|
33
|
+
|
34
|
+
def each
|
35
|
+
@data.each
|
36
|
+
end
|
37
|
+
|
38
|
+
def keys
|
39
|
+
@data.keys
|
40
|
+
end
|
41
|
+
|
42
|
+
def save
|
43
|
+
@log.debug "Saving #{@data['id']}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def id
|
47
|
+
@data['id']
|
48
|
+
end
|
49
|
+
|
50
|
+
def realm
|
51
|
+
@data['realm']
|
52
|
+
end
|
53
|
+
|
54
|
+
def domain
|
55
|
+
@data['domain']
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_s
|
59
|
+
"#{@data['id']}"
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
#!ruby
|
2
|
+
|
3
|
+
require 'noms/command/version'
|
4
|
+
|
5
|
+
require 'httpclient'
|
6
|
+
require 'etc'
|
7
|
+
require 'highline/import'
|
8
|
+
require 'json'
|
9
|
+
require 'cgi'
|
10
|
+
|
11
|
+
require 'noms/command/base'
|
12
|
+
require 'noms/command/auth/identity'
|
13
|
+
|
14
|
+
class NOMS
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
class NOMS::Command
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
class NOMS::Command::Auth < NOMS::Command::Base
|
23
|
+
|
24
|
+
def initialize(opts={})
|
25
|
+
@log = opts[:logger] || default_logger
|
26
|
+
@loaded = { }
|
27
|
+
(opts[:specified_identities] || []).each do |file|
|
28
|
+
maybe_id = read_identity_from file
|
29
|
+
raise NOMS::Command::Error.now "#{file} contains invalid identity (no 'id')" unless
|
30
|
+
maybe_id['id']
|
31
|
+
@loaded[maybe_id['id']] = maybe_id
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def read_identity_from(file)
|
36
|
+
@log.debug "Reading identity file #{file}"
|
37
|
+
begin
|
38
|
+
# TODO: Encryption and passphrases
|
39
|
+
raise NOMS::Command::Error.new "Identity file #{file} does not exist" unless File.exist? file
|
40
|
+
s = File.stat file
|
41
|
+
raise NOMS::Command::Error.new "You don't own identity file #{file}" unless s.owned?
|
42
|
+
raise NOMS::Command::Error.new "Permissions on #{file} are too permissive" unless (s.mode & 077 == 0)
|
43
|
+
contents = File.read file
|
44
|
+
case contents[0].chr
|
45
|
+
when '{'
|
46
|
+
NOMS::Command::Auth::Identity.new(self, JSON.parse(contents))
|
47
|
+
else
|
48
|
+
raise NOMS::Command::Error.new "#{file} contains unsupported or corrupted data"
|
49
|
+
end
|
50
|
+
rescue StandardError => e
|
51
|
+
if e.is_a? NOMS::Command::Error
|
52
|
+
raise e
|
53
|
+
else
|
54
|
+
raise NOMS::Command::Error.new "Couldn't load identity from #{file} (#{e.class}): #{e.message}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# TODO: Persistent auth creds
|
60
|
+
# Store like a client certificate: encrypted. Then use an
|
61
|
+
# agent to store by using <agent>-add and typing passphrase
|
62
|
+
# just like a client cert. <agent> expires credentials.
|
63
|
+
# also you can explicitly unencrypt identity file
|
64
|
+
|
65
|
+
def load(url, response)
|
66
|
+
# Prompt
|
67
|
+
auth_header = response.header['www-authenticate']
|
68
|
+
auth_header = (auth_header.respond_to?(:first) ? auth_header.first : auth_header)
|
69
|
+
case auth_header
|
70
|
+
when /Basic/
|
71
|
+
if m = /realm=\"([^\"]*)\"/.match(auth_header)
|
72
|
+
realm = m[1]
|
73
|
+
else
|
74
|
+
realm = ''
|
75
|
+
end
|
76
|
+
domain = [url.scheme, '://', url.host, ':', url.port, '/'].join('')
|
77
|
+
identity_id = CGI.escape(realm) + '=' + domain
|
78
|
+
if saved(identity_id)
|
79
|
+
retrieve(identity_id)
|
80
|
+
else
|
81
|
+
if $stdin.tty?
|
82
|
+
default_user = Etc.getlogin
|
83
|
+
prompt = "#{domain} (#{realm}) username: "
|
84
|
+
user = ask(prompt) { |u| u.default = Etc.getlogin }
|
85
|
+
pass = ask('Password: ') { |p| p.echo = false }
|
86
|
+
NOMS::Command::Auth::Identity.new(self, {
|
87
|
+
'id' => identity_id,
|
88
|
+
'realm' => realm,
|
89
|
+
'domain' => domain,
|
90
|
+
'username' => user,
|
91
|
+
'password' => pass
|
92
|
+
})
|
93
|
+
else
|
94
|
+
@log.warn "Can't prompt for #{domain} (#{realm}) authentication (not a terminal)"
|
95
|
+
NOMS::Command::Auth::Identity.new({
|
96
|
+
'id' => identity_id,
|
97
|
+
'realm' => realm,
|
98
|
+
'domain' => domain,
|
99
|
+
'username' => '',
|
100
|
+
'password' => ''
|
101
|
+
})
|
102
|
+
end
|
103
|
+
end
|
104
|
+
else
|
105
|
+
raise NOMS::Command::Error.new "Authentication not supported: #{auth_header.inspect}"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def saved(identity_id)
|
110
|
+
@loaded.has_key? identity_id
|
111
|
+
end
|
112
|
+
|
113
|
+
def retrieve(identity_id)
|
114
|
+
@loaded[identity_id]
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
#!ruby
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
class NOMS
|
6
|
+
|
7
|
+
end
|
8
|
+
|
9
|
+
class NOMS::Command
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
class NOMS::Command::Base
|
14
|
+
|
15
|
+
def default_logger
|
16
|
+
log = Logger.new $stdin
|
17
|
+
log.level = Logger::WARN
|
18
|
+
log.level = Logger::DEBUG if ENV['NOMS_DEBUG']
|
19
|
+
log
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
#!ruby
|
2
|
+
|
3
|
+
require 'noms/command/error'
|
4
|
+
|
5
|
+
class NOMS
|
6
|
+
|
7
|
+
end
|
8
|
+
|
9
|
+
class NOMS::Command
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
class NOMS::Command::Document
|
14
|
+
|
15
|
+
attr_accessor :exitcode, :argv
|
16
|
+
|
17
|
+
def initialize(doc)
|
18
|
+
raise NOMS::Command::Error.new "Document type '#{docobj['$doctype']}' not understood" unless
|
19
|
+
doc['$doctype'] == 'noms-v2'
|
20
|
+
@doc = doc
|
21
|
+
end
|
22
|
+
|
23
|
+
# Make these synonymous with the keys
|
24
|
+
def body
|
25
|
+
@doc['$body']
|
26
|
+
end
|
27
|
+
|
28
|
+
def body=(rval)
|
29
|
+
@doc['$body'] = rval
|
30
|
+
end
|
31
|
+
|
32
|
+
def script
|
33
|
+
@doc['$script']
|
34
|
+
end
|
35
|
+
|
36
|
+
def script=(rval)
|
37
|
+
@doc['$script'] = rval
|
38
|
+
end
|
39
|
+
|
40
|
+
def argv
|
41
|
+
@doc['$argv']
|
42
|
+
end
|
43
|
+
|
44
|
+
def argv=(rval)
|
45
|
+
@doc['$argv'] = rval
|
46
|
+
end
|
47
|
+
|
48
|
+
def exitcode
|
49
|
+
@doc['$exitcode']
|
50
|
+
end
|
51
|
+
|
52
|
+
def exitcode=(rval)
|
53
|
+
unless rval.respond_to?(:to_int) and rval <= 255 and rval >= 0
|
54
|
+
raise NOMS::Command::Error.new "Exitcode ${rval.inspect} out of range"
|
55
|
+
end
|
56
|
+
@doc['$exitcode'] = rval
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
#!ruby
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'yaml'
|
5
|
+
require 'csv'
|
6
|
+
|
7
|
+
require 'noms/command/error'
|
8
|
+
|
9
|
+
class NOMS
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
class NOMS::Command
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
class NOMS::Command::Formatter
|
18
|
+
|
19
|
+
def initialize(data=nil, opt={})
|
20
|
+
@data = data
|
21
|
+
@format_raw_object = opt[:format_raw_object] || lambda { |o| o.to_yaml }
|
22
|
+
end
|
23
|
+
|
24
|
+
def render(item=@data)
|
25
|
+
if item.nil?
|
26
|
+
''
|
27
|
+
elsif item.respond_to? :to_ary
|
28
|
+
item.map { |it| render it }.join("\n")
|
29
|
+
elsif item.respond_to? :has_key?
|
30
|
+
if item['$type']
|
31
|
+
case item['$type']
|
32
|
+
when 'object-list'
|
33
|
+
render_object_list item
|
34
|
+
when 'object'
|
35
|
+
render_object item
|
36
|
+
end
|
37
|
+
else
|
38
|
+
# It's a raw object, do YAML
|
39
|
+
@format_raw_object.call item
|
40
|
+
end
|
41
|
+
else
|
42
|
+
item.to_s
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def _fmt(spec)
|
47
|
+
'%' +
|
48
|
+
((spec['align'] && spec['align'] == 'right') ? '' : '-') +
|
49
|
+
(spec['width'] ? spec['width'].to_s : '') +
|
50
|
+
(spec['maxwidth'] ? '.' + spec['maxwidth'] : '') +
|
51
|
+
's'
|
52
|
+
end
|
53
|
+
|
54
|
+
def _fmth(spec)
|
55
|
+
# Headers are always left-aligned
|
56
|
+
'%-' +
|
57
|
+
(spec['width'] ? spec['width'].to_s : '') +
|
58
|
+
(spec['maxwidth'] ? '.' + spec['maxwidth'] : '') +
|
59
|
+
's'
|
60
|
+
end
|
61
|
+
|
62
|
+
def render_object_list(objlist)
|
63
|
+
objlist['$labels'] ||= true
|
64
|
+
objlist['$format'] ||= 'lines'
|
65
|
+
raise NOMS::Command::Error.new("objectlist ('lines' format) must contain '$columns' list") unless
|
66
|
+
objlist['$columns'] and objlist['$columns'].respond_to? :map
|
67
|
+
|
68
|
+
case objlist['$format']
|
69
|
+
when 'lines'
|
70
|
+
render_object_lines objlist
|
71
|
+
when 'yaml'
|
72
|
+
filter_object_list(objlist).to_yaml
|
73
|
+
when 'json'
|
74
|
+
JSON.pretty_generate(filter_object_list(objlist))
|
75
|
+
when 'csv'
|
76
|
+
render_csv objlist
|
77
|
+
else
|
78
|
+
raise NOMS::Command::Error.new("objectlist format '#{objlist['$format']}' not supported")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def filter_object_list(objlist)
|
83
|
+
columns = normalize_columns objlist['$columns']
|
84
|
+
|
85
|
+
objlist['$data'].map do |object|
|
86
|
+
Hash[columns.map { |c| [c['heading'], object[c['field']]] }]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def normalize_columns(cols)
|
91
|
+
cols.map do |spec|
|
92
|
+
new_spec = { }
|
93
|
+
if spec.respond_to? :has_key?
|
94
|
+
new_spec.merge! spec
|
95
|
+
raise NOMS::Command::Error.new("Column must contain 'field': #{spec.inspect}") unless
|
96
|
+
spec['field']
|
97
|
+
new_spec['heading'] ||= new_spec['field']
|
98
|
+
else
|
99
|
+
new_spec = {
|
100
|
+
'field' => spec,
|
101
|
+
'heading' => spec
|
102
|
+
}
|
103
|
+
end
|
104
|
+
new_spec
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def render_csv(objlist)
|
109
|
+
labels = objlist.has_key?('$labels') ? objlist['$labels'] : true
|
110
|
+
|
111
|
+
columns = normalize_columns(objlist['$columns'] || [])
|
112
|
+
|
113
|
+
CSV.generate do |csv|
|
114
|
+
csv << columns.map { |f| f['heading'] } if labels
|
115
|
+
objlist['$data'].each do |object|
|
116
|
+
csv << columns.map { |f| _string(object[f['field']]) }
|
117
|
+
end
|
118
|
+
end.chomp
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
def render_object_lines(objlist)
|
124
|
+
columns = normalize_columns(objlist['$columns'] || [])
|
125
|
+
labels = objlist.has_key?('$labels') ? objlist['$labels'] : true
|
126
|
+
|
127
|
+
header_fmt = columns.map { |f| _fmth f }.join(' ')
|
128
|
+
fmt = columns.map { |f| _fmt f }.join(' ')
|
129
|
+
|
130
|
+
header_cells = columns.map { |f| f['heading'] }
|
131
|
+
out = labels ? [ sprintf(header_fmt, *header_cells) ] : []
|
132
|
+
|
133
|
+
out += objlist['$data'].map do |object|
|
134
|
+
cells = columns.map { |f| _string(object[f['field']]) }
|
135
|
+
sprintf(fmt, *cells)
|
136
|
+
end
|
137
|
+
|
138
|
+
out.join("\n")
|
139
|
+
|
140
|
+
end
|
141
|
+
|
142
|
+
def _string(datum)
|
143
|
+
datum.kind_of?(Enumerable) ? datum.to_json : datum.to_s
|
144
|
+
end
|
145
|
+
|
146
|
+
def render_object(object)
|
147
|
+
object['$format'] ||= 'record'
|
148
|
+
|
149
|
+
case object['$format']
|
150
|
+
when 'record'
|
151
|
+
render_object_record object
|
152
|
+
when 'json'
|
153
|
+
JSON.pretty_generate(filter_object(object))
|
154
|
+
when 'yaml'
|
155
|
+
filter_object(object).to_yaml
|
156
|
+
else
|
157
|
+
raise NOMS::Command::Error.new("object format '#{object['$format']}' not supported")
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def render_object_record(object)
|
162
|
+
labels = object.has_key?('$labels') ? object['$labels'] : true
|
163
|
+
fields = (object['$fields'] || object['$data'].keys).sort
|
164
|
+
data = object['$data']
|
165
|
+
fields.map do |field|
|
166
|
+
(labels ? (field + ': ') : '' ) + _string(data[field])
|
167
|
+
end.join("\n")
|
168
|
+
end
|
169
|
+
|
170
|
+
def filter_object(object)
|
171
|
+
if object['$fields']
|
172
|
+
Hash[object['$fields'].map { |f| [f, object['$data'][f]] }]
|
173
|
+
else
|
174
|
+
object['$data']
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|