kamerling 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +50 -0
  3. data/.ruby-version +1 -0
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +90 -0
  6. data/LICENCE +661 -0
  7. data/README.md +6 -0
  8. data/Rakefile +18 -0
  9. data/bin/kamerling +9 -0
  10. data/config/reek.yml +18 -0
  11. data/kamerling.gemspec +30 -0
  12. data/lib/kamerling/addr.rb +18 -0
  13. data/lib/kamerling/client.rb +2 -0
  14. data/lib/kamerling/core_extensions/main.rb +17 -0
  15. data/lib/kamerling/core_extensions.rb +1 -0
  16. data/lib/kamerling/handler.rb +24 -0
  17. data/lib/kamerling/http_api.rb +38 -0
  18. data/lib/kamerling/logging.rb +20 -0
  19. data/lib/kamerling/message.rb +47 -0
  20. data/lib/kamerling/migrations/1_basic_schema.rb +45 -0
  21. data/lib/kamerling/net_dispatcher.rb +8 -0
  22. data/lib/kamerling/project.rb +2 -0
  23. data/lib/kamerling/receiver.rb +11 -0
  24. data/lib/kamerling/registrar.rb +9 -0
  25. data/lib/kamerling/registration.rb +2 -0
  26. data/lib/kamerling/repo.rb +32 -0
  27. data/lib/kamerling/repos.rb +65 -0
  28. data/lib/kamerling/result.rb +2 -0
  29. data/lib/kamerling/server/http.rb +26 -0
  30. data/lib/kamerling/server/sock.rb +32 -0
  31. data/lib/kamerling/server/tcp.rb +19 -0
  32. data/lib/kamerling/server/udp.rb +20 -0
  33. data/lib/kamerling/server_runner.rb +50 -0
  34. data/lib/kamerling/task.rb +2 -0
  35. data/lib/kamerling/task_dispatcher.rb +29 -0
  36. data/lib/kamerling/uuid.rb +17 -0
  37. data/lib/kamerling/uuid_object.rb +79 -0
  38. data/lib/kamerling/views/clients.slim +6 -0
  39. data/lib/kamerling/views/layout.slim +6 -0
  40. data/lib/kamerling/views/project.slim +11 -0
  41. data/lib/kamerling/views/projects.slim +9 -0
  42. data/lib/kamerling/views/root.slim +6 -0
  43. data/lib/kamerling.rb +29 -0
  44. data/spec/kamerling/addr_spec.rb +27 -0
  45. data/spec/kamerling/client_spec.rb +9 -0
  46. data/spec/kamerling/core_extensions/main_spec.rb +18 -0
  47. data/spec/kamerling/handler_spec.rb +30 -0
  48. data/spec/kamerling/http_api_spec.rb +93 -0
  49. data/spec/kamerling/logging_spec.rb +73 -0
  50. data/spec/kamerling/message_spec.rb +70 -0
  51. data/spec/kamerling/net_dispatcher_spec.rb +21 -0
  52. data/spec/kamerling/receiver_spec.rb +22 -0
  53. data/spec/kamerling/registrar_spec.rb +16 -0
  54. data/spec/kamerling/repo_spec.rb +60 -0
  55. data/spec/kamerling/repos_spec.rb +136 -0
  56. data/spec/kamerling/server/http_spec.rb +22 -0
  57. data/spec/kamerling/server/tcp_spec.rb +46 -0
  58. data/spec/kamerling/server/udp_spec.rb +44 -0
  59. data/spec/kamerling/server_runner_spec.rb +65 -0
  60. data/spec/kamerling/task_dispatcher_spec.rb +23 -0
  61. data/spec/kamerling/task_spec.rb +9 -0
  62. data/spec/kamerling/uuid_object_spec.rb +101 -0
  63. data/spec/kamerling/uuid_spec.rb +24 -0
  64. data/spec/spec_helper.rb +26 -0
  65. metadata +325 -0
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ require 'rake/testtask'
2
+ require 'reek/rake/task'
3
+ require 'rubocop/rake_task'
4
+
5
+ task default: %i[spec rubocop reek]
6
+
7
+ Rake::TestTask.new :spec do |task|
8
+ task.test_files = FileList['spec/**/*_spec.rb']
9
+ task.warning = true
10
+ end
11
+
12
+ Reek::Rake::Task.new do |task|
13
+ task.config_files = 'config/reek.yml'
14
+ task.fail_on_error = false
15
+ task.reek_opts = '--quiet'
16
+ end
17
+
18
+ Rubocop::RakeTask.new
data/bin/kamerling ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/kamerling'
4
+
5
+ Thread.abort_on_exception = true
6
+
7
+ Kamerling::Logging.log_to Logger.new $stdout
8
+
9
+ Kamerling::ServerRunner.new(ARGV).start.join
data/config/reek.yml ADDED
@@ -0,0 +1,18 @@
1
+ DuplicateMethodCall:
2
+ exclude:
3
+ - Kamerling#self.class_definition_from
4
+ - Kamerling::Handler#handle
5
+
6
+ IrresponsibleModule:
7
+ enabled: false
8
+
9
+ NestedIterators:
10
+ max_allowed_nesting: 2
11
+
12
+ UncommunicativeMethodName:
13
+ exclude:
14
+ - Kamerling#self.UUIDObject
15
+
16
+ UnusedParameters:
17
+ exclude:
18
+ - Kamerling::Server::HTTP#initialize
data/kamerling.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ Gem::Specification.new do |gem|
2
+ gem.author = 'Piotr Szotkowski'
3
+ gem.description = 'A network server for distributing computations across different clients speaking TCP or UDP.'
4
+ gem.email = 'p.szotkowski@tele.pw.edu.pl'
5
+ gem.homepage = 'https://github.com/chastell/kamerling'
6
+ gem.license = 'AGPL-3.0'
7
+ gem.name = 'kamerling'
8
+ gem.summary = 'Kamerling: a computation network server'
9
+ gem.version = '0.0.1'
10
+
11
+ gem.files = `git ls-files -z`.split "\0"
12
+ gem.executables = gem.files.grep(%r{^bin/}).map { |path| File.basename path }
13
+ gem.test_files = gem.files.grep %r{^spec/.*\.rb$}
14
+
15
+ gem.add_dependency 'after_do', '~> 0.3.0'
16
+ gem.add_dependency 'sequel', '~> 4.4'
17
+ gem.add_dependency 'sinatra', '~> 1.4'
18
+ gem.add_dependency 'slim', '~> 2.0'
19
+ gem.add_dependency 'sqlite3', '~> 1.3'
20
+
21
+ gem.add_development_dependency 'bogus', '~> 0.1.3'
22
+ gem.add_development_dependency 'minitest', '~> 5.0'
23
+ gem.add_development_dependency 'minitest-focus', '~> 1.1'
24
+ gem.add_development_dependency 'nokogiri', '~> 1.6'
25
+ gem.add_development_dependency 'rack-test', '~> 0.6.2'
26
+ gem.add_development_dependency 'rake', '~> 10.1'
27
+ gem.add_development_dependency 'reek', '~> 1.3'
28
+ gem.add_development_dependency 'rerun', '~> 0.8.2'
29
+ gem.add_development_dependency 'rubocop', '~> 0.18.0'
30
+ end
@@ -0,0 +1,18 @@
1
+ module Kamerling
2
+ Addr = Struct.new :host, :port, :prot do
3
+ def connectable?
4
+ TCPSocket.open(*self).close
5
+ true
6
+ rescue Errno::ECONNREFUSED
7
+ false
8
+ end
9
+
10
+ def to_a
11
+ [host, port]
12
+ end
13
+
14
+ def to_s
15
+ "#{host}:#{port} (#{prot})"
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,2 @@
1
+ module Kamerling class Client < UUIDObject :addr, busy: false
2
+ end end
@@ -0,0 +1,17 @@
1
+ module Kamerling module CoreExtensions module Main
2
+ module_function
3
+
4
+ def req param
5
+ method = caller.first[/`(.*)'$/, 1]
6
+ callsite = Class === self ? "#{name}.#{method}" : "#{self.class}##{method}"
7
+ raise "#{callsite}: param #{param} is required"
8
+ end
9
+
10
+ def warn_off
11
+ verbose = $VERBOSE
12
+ $VERBOSE = false
13
+ yield
14
+ ensure
15
+ $VERBOSE = verbose
16
+ end
17
+ end end end
@@ -0,0 +1 @@
1
+ require_relative 'core_extensions/main'
@@ -0,0 +1,24 @@
1
+ module Kamerling class Handler
2
+ UnknownInput = Class.new RuntimeError
3
+
4
+ def initialize receiver: Receiver.new, registrar: Registrar.new
5
+ @receiver, @registrar = receiver, registrar
6
+ end
7
+
8
+ def handle input, addr
9
+ message = Message.new input
10
+ case message.type
11
+ when :RGST
12
+ registrar.register addr: addr, client_uuid: message.client_uuid,
13
+ project_uuid: message.project_uuid
14
+ when :RSLT
15
+ receiver.receive addr: addr, client_uuid: message.client_uuid,
16
+ data: message.payload, task_uuid: message.task_uuid
17
+ end
18
+ rescue Message::UnknownType => exception
19
+ raise UnknownInput, exception.message
20
+ end
21
+
22
+ attr_reader :receiver, :registrar
23
+ private :receiver, :registrar
24
+ end end
@@ -0,0 +1,38 @@
1
+ require 'forwardable'
2
+ require 'sinatra/base'
3
+ require 'slim'
4
+
5
+ module Kamerling class HTTPAPI < Sinatra::Base
6
+ extend Forwardable
7
+
8
+ configure { set repos: Repos }
9
+
10
+ get '/' do
11
+ slim :root
12
+ end
13
+
14
+ get '/clients' do
15
+ warn_off { slim :clients, locals: { clients: repos.clients } }
16
+ end
17
+
18
+ get '/projects' do
19
+ warn_off { slim :projects, locals: { projects: repos.projects } }
20
+ end
21
+
22
+ get '/projects/:project_uuid' do
23
+ project = repos.project params['project_uuid']
24
+ clients = repos.clients_for project
25
+ tasks = repos.tasks_for project
26
+ warn_off { slim :project, locals: { clients: clients, tasks: tasks } }
27
+ end
28
+
29
+ post '/projects' do
30
+ uuid = params.fetch('uuid') { UUID.new }
31
+ repos << Project.new(name: params['name'], uuid: uuid)
32
+ redirect '/projects'
33
+ end
34
+
35
+ private
36
+
37
+ delegate repos: :settings
38
+ end end
@@ -0,0 +1,20 @@
1
+ require 'after_do'
2
+ require 'logger'
3
+
4
+ module Kamerling module Logging
5
+ module_function
6
+
7
+ def log_to logger: Logger.new($stdout)
8
+ Server::Sock.extend AfterDo
9
+ Server::Sock.before :start do |*, server|
10
+ logger.info "start #{server.addr}"
11
+ end
12
+ Server::Sock.before :handle do |input, client_addr|
13
+ logger.info "connect #{client_addr}"
14
+ logger.debug "received #{client_addr} #{input}"
15
+ end
16
+ Server::Sock.after :stop do |*, server|
17
+ logger.info "stop #{server.addr}"
18
+ end
19
+ end
20
+ end end
@@ -0,0 +1,47 @@
1
+ module Kamerling class Message
2
+ KnownTypes = %w[DATA PING RGST RSLT]
3
+ UnknownType = Class.new RuntimeError
4
+
5
+ def self.[](client: req(:client), payload: req(:payload),
6
+ project: req(:project), task: req(:task), type: req(:type))
7
+ new "#{type}\0\0\0\0\0\0\0\0\0\0\0\0" + UUID.bin(client.uuid) +
8
+ UUID.bin(project.uuid) + UUID.bin(task.uuid) + payload
9
+ end
10
+
11
+ def initialize raw
12
+ @raw = raw
13
+ type = raw[0..3]
14
+ raise UnknownType, type unless KnownTypes.include? type or type.empty?
15
+ end
16
+
17
+ def == other
18
+ raw == other.raw
19
+ end
20
+
21
+ def client_uuid
22
+ UUID[raw[16..31]]
23
+ end
24
+
25
+ def payload
26
+ raw[64..-1]
27
+ end
28
+
29
+ def project_uuid
30
+ UUID[raw[32..47]]
31
+ end
32
+
33
+ def task_uuid
34
+ UUID[raw[48..63]]
35
+ end
36
+
37
+ def to_s
38
+ raw
39
+ end
40
+
41
+ def type
42
+ raw[0..3].to_sym
43
+ end
44
+
45
+ attr_reader :raw
46
+ private :raw
47
+ end end
@@ -0,0 +1,45 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table :clients do
4
+ uuid :uuid, primary_key: true
5
+ boolean :busy, null: false
6
+ inet :host, null: false
7
+ integer :port, null: false
8
+ string :prot, null: false
9
+ end
10
+
11
+ create_table :projects do
12
+ uuid :uuid, primary_key: true
13
+ string :name, null: false
14
+ end
15
+
16
+ create_table :registrations do
17
+ uuid :uuid, primary_key: true
18
+ inet :host, null: false
19
+ integer :port, null: false
20
+ string :prot, null: false
21
+ foreign_key :client_uuid, :clients, index: true, null: false,
22
+ type: :uuid
23
+ foreign_key :project_uuid, :projects, index: true, null: false,
24
+ type: :uuid
25
+ end
26
+
27
+ create_table :results do
28
+ uuid :uuid, primary_key: true
29
+ bytea :data, null: false
30
+ inet :host, null: false
31
+ integer :port, null: false
32
+ string :prot, null: false
33
+ foreign_key :client_uuid, :clients, index: true, null: false, type: :uuid
34
+ foreign_key :task_uuid, :tasks, index: true, null: false, type: :uuid
35
+ end
36
+
37
+ create_table :tasks do
38
+ uuid :uuid, primary_key: true
39
+ boolean :done, null: false
40
+ bytea :data, null: false
41
+ foreign_key :project_uuid, :projects, index: true, null: false,
42
+ type: :uuid
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,8 @@
1
+ module Kamerling class NetDispatcher
2
+ def dispatch addr, bytes
3
+ case addr.prot
4
+ when :TCP then TCPSocket.open(*addr) { |socket| socket << bytes }
5
+ when :UDP then UDPSocket.new.send bytes, 0, *addr
6
+ end
7
+ end
8
+ end end
@@ -0,0 +1,2 @@
1
+ module Kamerling class Project < UUIDObject :name
2
+ end end
@@ -0,0 +1,11 @@
1
+ module Kamerling class Receiver
2
+ def receive addr: req(:addr), client_uuid: req(:client_uuid),
3
+ data: req(:data), repos: Repos, task_uuid: req(:task_uuid)
4
+ client = repos[Client][client_uuid]
5
+ task = repos[Task][task_uuid]
6
+ result = Result.new addr: addr, client: client, data: data, task: task
7
+ client.busy = false
8
+ task.done = true
9
+ repos << result << client << task
10
+ end
11
+ end end
@@ -0,0 +1,9 @@
1
+ module Kamerling class Registrar
2
+ def register addr: req(:addr), client_uuid: req(:client_uuid),
3
+ project_uuid: req(:project_uuid), repos: Repos
4
+ client = repos[Client][client_uuid]
5
+ project = repos[Project][project_uuid]
6
+ reg = Registration.new addr: addr, client: client, project: project
7
+ repos[Registration] << reg
8
+ end
9
+ end end
@@ -0,0 +1,2 @@
1
+ module Kamerling class Registration < UUIDObject :addr, :client, :project
2
+ end end
@@ -0,0 +1,32 @@
1
+ module Kamerling class Repo
2
+ NotFound = Class.new RuntimeError
3
+
4
+ def initialize klass, source
5
+ @klass, @source = klass, source
6
+ end
7
+
8
+ def << object
9
+ hash = object.to_h
10
+ warn_off { source << hash }
11
+ rescue Sequel::UniqueConstraintViolation
12
+ warn_off { source.where(uuid: object.uuid).update hash }
13
+ end
14
+
15
+ def [] uuid
16
+ hash = warn_off { source[uuid: uuid] }
17
+ raise NotFound, "#{klass} with UUID #{uuid}" unless hash
18
+ klass.from_h hash
19
+ end
20
+
21
+ def all
22
+ source.all.map { |hash| klass.from_h hash }
23
+ end
24
+
25
+ def related_to object
26
+ key = "#{object.class.name.split('::').last.downcase}_uuid".to_sym
27
+ source.where(key => object.uuid).map { |hash| klass.from_h hash }
28
+ end
29
+
30
+ attr_reader :klass, :source
31
+ private :klass, :source
32
+ end end
@@ -0,0 +1,65 @@
1
+ warn_off { require 'sequel' }
2
+
3
+ Sequel.extension :migration
4
+
5
+ module Kamerling class Repos
6
+ class << self
7
+ attr_writer :repos
8
+
9
+ def << object
10
+ repos[object.class] << object
11
+ self
12
+ end
13
+
14
+ def [] klass
15
+ repos[klass]
16
+ end
17
+
18
+ def clients
19
+ repos[Client].all
20
+ end
21
+
22
+ def clients_for project
23
+ repos[Registration].related_to(project).map(&:client)
24
+ end
25
+
26
+ def db= db
27
+ warn_off { Sequel::Migrator.run db, "#{__dir__}/migrations" }
28
+ @repos = nil
29
+ @db = db
30
+ end
31
+
32
+ def free_clients_for project
33
+ clients_for(project).reject(&:busy)
34
+ end
35
+
36
+ def next_task_for project
37
+ repos[Task].related_to(project).reject(&:done).first
38
+ end
39
+
40
+ def project project_uuid
41
+ repos[Project][project_uuid]
42
+ end
43
+
44
+ def projects
45
+ repos[Project].all
46
+ end
47
+
48
+ def tasks_for project
49
+ repos[Task].related_to project
50
+ end
51
+
52
+ private
53
+
54
+ def db
55
+ @db ||= self.db = Sequel.sqlite
56
+ end
57
+
58
+ def repos
59
+ @repos ||= Hash.new do |repos, klass|
60
+ table = "#{klass.name.split('::').last.downcase}s".to_sym
61
+ repos[klass] = Repo.new klass, warn_off { db[table] }
62
+ end
63
+ end
64
+ end
65
+ end end
@@ -0,0 +1,2 @@
1
+ module Kamerling class Result < UUIDObject :addr, :client, :data, :task
2
+ end end
@@ -0,0 +1,26 @@
1
+ module Kamerling module Server class HTTP
2
+ attr_reader :addr
3
+
4
+ def initialize addr: req(:addr)
5
+ @addr = addr
6
+ end
7
+
8
+ def join
9
+ thread.join
10
+ end
11
+
12
+ def start
13
+ @thread = Thread.new do
14
+ Rack::Handler::WEBrick.run HTTPAPI, Host: addr.host, Port: addr.port
15
+ end
16
+ loop { break if addr.connectable? }
17
+ self
18
+ end
19
+
20
+ def stop
21
+ thread.exit.join
22
+ end
23
+
24
+ attr_reader :thread
25
+ private :thread
26
+ end end end
@@ -0,0 +1,32 @@
1
+ module Kamerling module Server class Sock
2
+ attr_reader :addr
3
+
4
+ def initialize addr: req(:addr), handler: Handler.new
5
+ @addr = addr
6
+ @handler = handler
7
+ end
8
+
9
+ def join
10
+ thread.join
11
+ end
12
+
13
+ def start
14
+ @thread = Thread.new { run_loop }
15
+ wait_till_started
16
+ self
17
+ end
18
+
19
+ def stop
20
+ thread.exit.join
21
+ end
22
+
23
+ attr_reader :handler, :thread
24
+ private :handler, :thread
25
+
26
+ private
27
+
28
+ def handle input, client_addr
29
+ handler.handle input, client_addr
30
+ rescue Handler::UnknownInput
31
+ end
32
+ end end end
@@ -0,0 +1,19 @@
1
+ module Kamerling module Server class TCP < Sock
2
+ private
3
+
4
+ def handle_connection socket
5
+ client_addr = Addr[*socket.remote_address.ip_unpack, :TCP]
6
+ input = socket.read
7
+ handle input, client_addr
8
+ ensure
9
+ socket.close
10
+ end
11
+
12
+ def run_loop
13
+ Socket.tcp_server_loop(*addr) { |socket| handle_connection socket }
14
+ end
15
+
16
+ def wait_till_started
17
+ loop { break if addr.connectable? }
18
+ end
19
+ end end end
@@ -0,0 +1,20 @@
1
+ module Kamerling module Server class UDP < Sock
2
+ private
3
+
4
+ def handle_connection socket
5
+ input, conn = socket.recvfrom 2**16
6
+ client_addr = Addr[conn[3], conn[1], :UDP]
7
+ handle input, client_addr
8
+ end
9
+
10
+ def run_loop
11
+ socket = UDPSocket.new.tap { |server| server.bind(*addr) }
12
+ loop { handle_connection socket if IO.select [socket] }
13
+ ensure
14
+ socket.close if socket
15
+ end
16
+
17
+ def wait_till_started
18
+ 200.times { thread.run }
19
+ end
20
+ end end end
@@ -0,0 +1,50 @@
1
+ require 'optparse'
2
+
3
+ module Kamerling class ServerRunner
4
+ Settings = Struct.new(*%i[db host http tcp udp])
5
+
6
+ def initialize args, classes: def_classes, orm: Sequel, repos: Repos
7
+ @args = args
8
+ repos.db = orm.connect settings.db
9
+ @servers = { http: :TCP, tcp: :TCP, udp: :UDP }.map do |type, prot|
10
+ addr = Addr[settings.host, settings.send(type), prot]
11
+ classes[type].new addr: addr if addr.port
12
+ end.compact
13
+ end
14
+
15
+ def join
16
+ servers.each(&:join)
17
+ end
18
+
19
+ def start
20
+ servers.each(&:start)
21
+ self
22
+ end
23
+
24
+ attr_reader :args, :servers
25
+ private :args, :servers
26
+
27
+ private
28
+
29
+ def def_classes
30
+ { http: Server::HTTP, tcp: Server::TCP, udp: Server::UDP }
31
+ end
32
+
33
+ def settings
34
+ @settings ||= Settings.new.tap do |sets|
35
+ sets.db = 'sqlite::memory:'
36
+ sets.host = '127.0.0.1'
37
+ OptionParser.new do |opts|
38
+ opts.on("--db #{sets.db}", String, 'database') do |db|
39
+ sets.db = db
40
+ end
41
+ opts.on("--host #{sets.host}", String, 'server host') do |host|
42
+ sets.host = host
43
+ end
44
+ opts.on('--http 0', Integer, 'HTTP port') { |http| sets.http = http }
45
+ opts.on('--tcp 0', Integer, 'TCP port') { |tcp| sets.tcp = tcp }
46
+ opts.on('--udp 0', Integer, 'UDP port') { |udp| sets.udp = udp }
47
+ end.parse! args
48
+ end
49
+ end
50
+ end end
@@ -0,0 +1,2 @@
1
+ module Kamerling class Task < UUIDObject :data, :project, done: false
2
+ end end
@@ -0,0 +1,29 @@
1
+ module Kamerling class TaskDispatcher
2
+ def initialize net_dispatcher: NetDispatcher.new, repos: Repos
3
+ @net_dispatcher = net_dispatcher
4
+ @repos = repos
5
+ end
6
+
7
+ def dispatch
8
+ repos.projects.each do |project|
9
+ repos.free_clients_for(project).each do |client|
10
+ task = repos.next_task_for(project)
11
+ dispatch_task client: client, project: project, task: task if task
12
+ end
13
+ end
14
+ end
15
+
16
+ attr_reader :net_dispatcher, :repos
17
+ private :net_dispatcher, :repos
18
+
19
+ private
20
+
21
+ def dispatch_task client: req(:client), project: req(:project),
22
+ task: req(:task)
23
+ message = Message[client: client, payload: task.data, project: project,
24
+ task: task, type: :DATA]
25
+ net_dispatcher.dispatch client.addr, message.to_s
26
+ client.busy = true
27
+ repos << client
28
+ end
29
+ end end
@@ -0,0 +1,17 @@
1
+ require 'securerandom'
2
+
3
+ module Kamerling module UUID
4
+ module_function
5
+
6
+ def [] bin
7
+ bin.unpack('H8H4H4H4H12').join '-'
8
+ end
9
+
10
+ def bin uuid
11
+ [uuid.tr('-', '')].pack 'H*'
12
+ end
13
+
14
+ def new
15
+ SecureRandom.uuid
16
+ end
17
+ end end