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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +191 -0
  5. data/README.rst +376 -0
  6. data/ROADMAP.rst +127 -0
  7. data/Rakefile +49 -0
  8. data/TODO.rst +7 -0
  9. data/bin/noms2 +20 -0
  10. data/fixture/dnc.rb +120 -0
  11. data/fixture/identity +5 -0
  12. data/fixture/public/dnc.json +22 -0
  13. data/fixture/public/echo.json +7 -0
  14. data/fixture/public/files/data.json +12 -0
  15. data/fixture/public/files/foo.json +2 -0
  16. data/fixture/public/lib/dnc.js +81 -0
  17. data/fixture/public/lib/noms-args.js +13 -0
  18. data/fixture/public/lib/showopt.js +18 -0
  19. data/fixture/public/location.json +8 -0
  20. data/fixture/public/showopt.json +15 -0
  21. data/fixture/rig2json +21 -0
  22. data/lib/noms/command/application.rb +204 -0
  23. data/lib/noms/command/auth/identity.rb +62 -0
  24. data/lib/noms/command/auth.rb +117 -0
  25. data/lib/noms/command/base.rb +22 -0
  26. data/lib/noms/command/document.rb +59 -0
  27. data/lib/noms/command/error.rb +11 -0
  28. data/lib/noms/command/formatter.rb +178 -0
  29. data/lib/noms/command/urinion/data.rb +63 -0
  30. data/lib/noms/command/urinion.rb +29 -0
  31. data/lib/noms/command/useragent.rb +134 -0
  32. data/lib/noms/command/version.rb +7 -0
  33. data/lib/noms/command/window.rb +95 -0
  34. data/lib/noms/command/xmlhttprequest.rb +181 -0
  35. data/lib/noms/command.rb +107 -0
  36. data/noms-command.gemspec +30 -0
  37. data/spec/01noms-command_spec.rb +30 -0
  38. data/spec/02noms-command.sh +31 -0
  39. data/spec/03application_spec.rb +47 -0
  40. data/spec/04application_spec.rb +61 -0
  41. data/spec/05formatter_spec.rb +195 -0
  42. data/spec/06urinion_data.rb +20 -0
  43. data/spec/07js_spec.rb +87 -0
  44. data/spec/08xhr_spec.rb +209 -0
  45. data/spec/09bookmarks_spec.rb +60 -0
  46. data/spec/10auth_spec.rb +33 -0
  47. data/spec/spec_helper.rb +40 -0
  48. 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,11 @@
1
+ class NOMS
2
+
3
+ end
4
+
5
+ class NOMS::Command
6
+
7
+ end
8
+
9
+ class NOMS::Command::Error < StandardError
10
+
11
+ 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