siffer 0.0.7 → 0.1.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.
- data/LICENSE +1 -1
- data/README +63 -93
- data/Rakefile +60 -0
- data/bin/siffer +12 -0
- data/lib/sif.rb +38 -0
- data/lib/sif/code_sets.rb +9 -0
- data/lib/sif/code_sets/access_permission_error_codes.rb +17 -0
- data/lib/sif/code_sets/authentication_error_codes.rb +15 -0
- data/lib/sif/code_sets/base_code_set.rb +75 -0
- data/lib/sif/code_sets/encryption_error_codes.rb +6 -0
- data/lib/sif/code_sets/error_category_codes.rb +20 -0
- data/lib/sif/code_sets/event_reporting_processing_error_codes.rb +7 -0
- data/lib/sif/code_sets/generic_message_handling_error_codes.rb +12 -0
- data/lib/sif/code_sets/provision_error_codes.rb +8 -0
- data/lib/sif/code_sets/registration_error_codes.rb +13 -0
- data/lib/sif/code_sets/request_response_error_codes.rb +20 -0
- data/lib/sif/code_sets/smb_error_codes.rb +9 -0
- data/lib/sif/code_sets/status_codes.rb +13 -0
- data/lib/sif/code_sets/subscription_error_codes.rb +7 -0
- data/lib/sif/code_sets/system_error_codes.rb +6 -0
- data/lib/sif/code_sets/transport_error_codes.rb +9 -0
- data/lib/sif/code_sets/xml_validation_error_codes.rb +10 -0
- data/lib/sif/config.rb +100 -0
- data/lib/sif/core_ext/array.rb +12 -0
- data/lib/sif/error.rb +34 -0
- data/lib/sif/exceptions.rb +9 -0
- data/lib/sif/messages.rb +3 -0
- data/lib/sif/messages/ack.rb +29 -0
- data/lib/sif/messages/message.rb +32 -0
- data/lib/sif/messages/register.rb +38 -0
- data/lib/sif/protocols.rb +15 -0
- data/lib/sif/status.rb +17 -0
- data/lib/siffer.rb +1 -41
- data/spec/base_code_set_spec.rb +35 -0
- data/spec/config_spec.rb +42 -0
- data/spec/error_spec.rb +11 -0
- data/spec/messages/ack_spec.rb +25 -0
- data/spec/messages/header_spec.rb +21 -0
- data/spec/messages/message_spec.rb +27 -0
- data/spec/messages/register_spec.rb +53 -0
- data/spec/protocol_spec.rb +11 -0
- data/spec/spec_helper.rb +8 -0
- metadata +66 -31
- data/lib/siffer/agent.rb +0 -53
- data/lib/siffer/core_ext/hash.rb +0 -15
- data/lib/siffer/messages.rb +0 -41
- data/lib/siffer/messages/ack.rb +0 -168
- data/lib/siffer/messages/event.rb +0 -20
- data/lib/siffer/messages/message.rb +0 -60
- data/lib/siffer/messages/provide.rb +0 -25
- data/lib/siffer/messages/provision.rb +0 -17
- data/lib/siffer/messages/register.rb +0 -50
- data/lib/siffer/messages/request.rb +0 -138
- data/lib/siffer/messages/response.rb +0 -48
- data/lib/siffer/messages/subscribe.rb +0 -17
- data/lib/siffer/messages/system_control.rb +0 -94
- data/lib/siffer/models.rb +0 -1
- data/lib/siffer/models/address.rb +0 -39
@@ -0,0 +1,8 @@
|
|
1
|
+
module Siffer
|
2
|
+
module CodeSets
|
3
|
+
class ProvisionErrorCode < BaseCodeSet; end
|
4
|
+
ProvisionErrorCode.register(1,:generic,"Generic error")
|
5
|
+
ProvisionErrorCode.register(3,:invalid_object,"Invalid object")
|
6
|
+
ProvisionErrorCode.register(4,:duplicate,"Object already has a provider (SIF_Provide message)")
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Siffer
|
2
|
+
module CodeSets
|
3
|
+
class RegistrationErrorCode < BaseCodeSet; end
|
4
|
+
RegistrationErrorCode.register(1,:generic,"Generic error")
|
5
|
+
RegistrationErrorCode.register(2,:source_invalid,"The SIF_SourceId is invalid")
|
6
|
+
RegistrationErrorCode.register(3,:protocol_not_supported,"Requested transport protocol is unsupported")
|
7
|
+
RegistrationErrorCode.register(4,:version_not_supported,"Requested SIF_Version(s) not supporrted")
|
8
|
+
RegistrationErrorCode.register(6,:max_buffer_size_too_small,"Requested SIF_MaxBufferSize is too small")
|
9
|
+
RegistrationErrorCode.register(7,:requires_secure_transport,"ZIS requires a secure transport")
|
10
|
+
RegistrationErrorCode.register(9,:registered_for_push_mode,"Agent is registered for push mode (returned when a push-mode agent sends a SIF_GetMessage)")
|
11
|
+
RegistrationErrorCode.register(10,:encoding_not_supported,"ZIS does not support the requested Accept-Encoding value")
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Siffer
|
2
|
+
module CodeSets
|
3
|
+
class RequestResponseErrorCode < BaseCodeSet; end
|
4
|
+
RequestResponseErrorCode.register(1,:generic,"Generic error")
|
5
|
+
RequestResponseErrorCode.register(3,:invalid_object,"Invalid Object")
|
6
|
+
RequestResponseErrorCode.register(4,:no_provider,"No provider")
|
7
|
+
RequestResponseErrorCode.register(7,:version_not_supported,"Responder does not support requested SIF_Version")
|
8
|
+
RequestResponseErrorCode.register(8,:buffer_size_not_supported,"Responder does not support requested SIF_MaxBufferSize")
|
9
|
+
RequestResponseErrorCode.register(9,:unsupported_query,"Unsupported query in request")
|
10
|
+
RequestResponseErrorCode.register(10,:invalid_msg_id,"Invalid SIF_RequestMsgId specified in SIF_Response")
|
11
|
+
RequestResponseErrorCode.register(11,:response_larger_than_buffer,"SIF_Response is larger than requested SIF_MaxBufferSize")
|
12
|
+
RequestResponseErrorCode.register(12,:invalid_packet_number,"SIF_PacketNumber is invalid in SIF_Response")
|
13
|
+
RequestResponseErrorCode.register(13,:response_does_not_match_version,"SIF_Response does not match any SIF_Version from SIF_Request")
|
14
|
+
RequestResponseErrorCode.register(14,:destination_does_not_match_source,"SIF_DestinationId does not match SIF_SourceId from SIF_Request")
|
15
|
+
RequestResponseErrorCode.register(15,:extended_query_not_supported,"No support for SIF_ExtendedQuery")
|
16
|
+
RequestResponseErrorCode.register(16,:deleted_due_to_timeout,"SIF_RequestMsgId deleted from cache due to timeout")
|
17
|
+
RequestResponseErrorCode.register(17,:deleted_by_administrator,"SIF_RequestMsgId deleted from cache by administrator")
|
18
|
+
RequestResponseErrorCode.register(18,:cancelled_by_agent,"SIF_Request cancelled by requesting agent")
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Siffer
|
2
|
+
module CodeSets
|
3
|
+
class SMBErrorCode < BaseCodeSet; end
|
4
|
+
SMBErrorCode.register(1,:generic,"Generic error")
|
5
|
+
SMBErrorCode.register(2,:invocation,"SMB can only be invoked during a SIF_Event acknowledgement")
|
6
|
+
SMBErrorCode.register(3,:final_expected,"Final SIF_Ack expected from Push-Mode Agent")
|
7
|
+
SMBErrorCode.register(4,:incorrect_msgid,"Incorrect SIF_MsgId in final SIF_Ack")
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Siffer
|
2
|
+
module CodeSets
|
3
|
+
class StatusCode < BaseCodeSet; end
|
4
|
+
StatusCode.register(0,:success,"Success (ZIS ONLY). SIF_Status/SIF_Data may contain additional data.")
|
5
|
+
StatusCode.register(1,:immediate,"Immediate SIF_Ack (AGENT ONLY). Message is persisted or processing is complete. Discard the referenced message.")
|
6
|
+
StatusCode.register(2,:intermediate,"Intermediate SIF_Ack (AGENT ONLY). Only valid in response to SIF_Event delivery. Invokes Selective Message Blocking. The event referenced must still be persisted, and no other events must be delivered, until the agent sends a \"Final\" SIF_Ack at a later time.")
|
7
|
+
StatusCode.register(3,:final,"Final SIF_Ack (AGENT ONLY). Sent (a SIF_Ack with this value is never returned by an agent in response to a delivered message) by an agent to the ZIS to end Selective Message Blocking. Discard the referenced event and allow for delivery of other events.")
|
8
|
+
StatusCode.register(7,:duplicate,"Already have a message with this SIF_MsgId from you.")
|
9
|
+
StatusCode.register(8,:sleeping,"Receiver is sleeping.")
|
10
|
+
StatusCode.register(9,:not_available,"No messages available. This is returned when an agent is trying to pull messages from a ZIS and there are no messages available.")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Siffer
|
2
|
+
module CodeSets
|
3
|
+
class TransportErrorCode < BaseCodeSet; end
|
4
|
+
TransportErrorCode.register(1,:generic,"Generic error")
|
5
|
+
TransportErrorCode.register(2,:protocol_not_supported,"Requested protocol is not supported")
|
6
|
+
TransportErrorCode.register(3,:secure_path_missing,"Secure channel requested and no secure path exists")
|
7
|
+
TransportErrorCode.register(4,:no_connection,"Unable to establish connection")
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Siffer
|
2
|
+
module CodeSets
|
3
|
+
class XmlValidationErrorCode < BaseCodeSet; end
|
4
|
+
XmlValidationErrorCode.register(1,:generic,"Generic error")
|
5
|
+
XmlValidationErrorCode.register(2,:not_well_formed,"Message is not well formed")
|
6
|
+
XmlValidationErrorCode.register(3,:validation,"Generic validation error")
|
7
|
+
XmlValidationErrorCode.register(4,:invalid_value,"Invalid value for element/attribute")
|
8
|
+
XmlValidationErrorCode.register(6,:missing,"Missing mandatory element/attribute")
|
9
|
+
end
|
10
|
+
end
|
data/lib/sif/config.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Siffer
|
4
|
+
class Config
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
# ==== Returns
|
9
|
+
# Hash:: The defaults for the config.
|
10
|
+
def defaults
|
11
|
+
@defaults ||= {
|
12
|
+
:host => "0.0.0.0",
|
13
|
+
:port => "4000",
|
14
|
+
:environment => "development",
|
15
|
+
:sif_root => Dir.pwd,
|
16
|
+
:session_id_key => "_session_id",
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
# Yields the configuration.
|
21
|
+
#
|
22
|
+
# ==== Block parameters
|
23
|
+
# c<Hash>:: The configuration parameters.
|
24
|
+
#
|
25
|
+
# ==== Examples
|
26
|
+
# H3O::SIF::Config.use do |config|
|
27
|
+
# config[:port] = 3100
|
28
|
+
# end
|
29
|
+
def use
|
30
|
+
@configuration ||= {}
|
31
|
+
yield @configuration
|
32
|
+
end
|
33
|
+
|
34
|
+
# ==== Parameters
|
35
|
+
# key<Object>:: The key to check.
|
36
|
+
#
|
37
|
+
# ==== Returns
|
38
|
+
# Boolean:: True if the key exists in the config.
|
39
|
+
def key?(key)
|
40
|
+
@configuration.key?(key)
|
41
|
+
end
|
42
|
+
|
43
|
+
# ==== Parameters
|
44
|
+
# key<Object>:: The key to retrieve the parameter for.
|
45
|
+
#
|
46
|
+
# ==== Returns
|
47
|
+
# Object:: The value of the configuration parameter.
|
48
|
+
def [](key)
|
49
|
+
(@configuration||={})[key]
|
50
|
+
end
|
51
|
+
|
52
|
+
# ==== Parameters
|
53
|
+
# key<Object>:: The key to set the parameter for.
|
54
|
+
# val<Object>:: The value of the parameter.
|
55
|
+
def []=(key,val)
|
56
|
+
@configuration[key] = val
|
57
|
+
end
|
58
|
+
|
59
|
+
# ==== Parameters
|
60
|
+
# key<Object>:: The key of the parameter to delete.
|
61
|
+
def delete(key)
|
62
|
+
@configuration.delete(key)
|
63
|
+
end
|
64
|
+
|
65
|
+
# ==== Parameters
|
66
|
+
# key<Object>:: The key to retrieve the parameter for.
|
67
|
+
# default<Object>::
|
68
|
+
# The default value to return if the parameter is not set.
|
69
|
+
#
|
70
|
+
# ==== Returns
|
71
|
+
# Object:: The value of the configuration parameter or the default.
|
72
|
+
def fetch(key, default)
|
73
|
+
@configuration.fetch(key, default)
|
74
|
+
end
|
75
|
+
|
76
|
+
# ==== Returns
|
77
|
+
# Hash:: The config as a hash.
|
78
|
+
def to_hash
|
79
|
+
@configuration
|
80
|
+
end
|
81
|
+
|
82
|
+
# ==== Returns
|
83
|
+
# String:: The config as YAML.
|
84
|
+
def to_yaml
|
85
|
+
@configuration.to_yaml
|
86
|
+
end
|
87
|
+
|
88
|
+
# Sets up the configuration by storing the given settings.
|
89
|
+
#
|
90
|
+
# ==== Parameters
|
91
|
+
# settings<Hash>::
|
92
|
+
# Configuration settings to use. These are merged with the defaults.
|
93
|
+
def setup(settings = {})
|
94
|
+
@configuration = defaults.merge(settings)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
data/lib/sif/error.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
module Siffer
|
2
|
+
class Error
|
3
|
+
|
4
|
+
attr_reader :category, :code, :desc, :extended_desc
|
5
|
+
|
6
|
+
def initialize(category, code, extended_desc = nil)
|
7
|
+
@code = case category
|
8
|
+
when :xml_validation then CodeSets::XmlValidationErrorCode[code]
|
9
|
+
when :encryption then CodeSets::EncryptionErrorCode[code]
|
10
|
+
when :authentication then CodeSets::AuthenticationErrorCode[code]
|
11
|
+
when :access_and_permissions then CodeSets::AccessPermissionErrorCode[code]
|
12
|
+
when :registration then CodeSets::RegistrationErrorCode[code]
|
13
|
+
when :provision then CodeSets::ProvisionErrorCode[code]
|
14
|
+
when :subscription then CodeSets::SubscriptionErrorCode[code]
|
15
|
+
when :request_and_response then CodeSets::RequestResponseErrorCode[code]
|
16
|
+
when :event_reporting_and_processing then CodeSets::EventReportingProcessingErrorCode[code]
|
17
|
+
when :transport then CodeSets::TransportErrorCode[code]
|
18
|
+
when :system then CodeSets::SystemErrorCode[code]
|
19
|
+
when :generic_message_handling then CodeSets::GenericMessageHandlingErrorCode[code]
|
20
|
+
when :smb_handling then CodeSets::SMBErrorCode[code]
|
21
|
+
else raise Exceptions::InvalidErrorCategory, "Invalid Error Category provided."
|
22
|
+
end
|
23
|
+
@category = CodeSets::ErrorCategoryCode[category]
|
24
|
+
@desc = @code.description
|
25
|
+
@extended_desc = extended_desc unless extended_desc.nil?
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
"#{@category}: #{@desc}"
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
data/lib/sif/messages.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
module Siffer
|
2
|
+
module Messages
|
3
|
+
|
4
|
+
class Ack < Message
|
5
|
+
|
6
|
+
attr_reader :original_source_id, :original_message_id, :status, :error
|
7
|
+
|
8
|
+
# source:: source id of this message
|
9
|
+
# original_source_id:: the source id of the message being acknowledged
|
10
|
+
# original_message_id:: the message id of the message being acknowledged
|
11
|
+
# status:: the status of the acknowledgement
|
12
|
+
# error:: the error that occured
|
13
|
+
def initialize(source, *args)
|
14
|
+
super(source)
|
15
|
+
@options = args.extract_options!
|
16
|
+
@original_source_id = @options[:original_source_id]
|
17
|
+
@original_message_id = @options[:original_message_id]
|
18
|
+
@status = @options[:status] unless @options[:status].nil?
|
19
|
+
@error = @options[:error] unless @options[:error].nil?
|
20
|
+
end
|
21
|
+
|
22
|
+
def error?
|
23
|
+
!@error.nil?
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Siffer
|
2
|
+
module Messages
|
3
|
+
# Base class for all Messages in the framework.
|
4
|
+
class Message
|
5
|
+
|
6
|
+
attr_reader :xmlns, :version, :header
|
7
|
+
|
8
|
+
def initialize(source)
|
9
|
+
raise MissingSource, "Source not provided." if source.nil?
|
10
|
+
@xmlns = Siffer.sif_xmlns
|
11
|
+
@version = Siffer.sif_version
|
12
|
+
@header = Header.new(source)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Each Message requires a Header to identify the source and time as well
|
16
|
+
# as provide an ID for the message itself.
|
17
|
+
class Header
|
18
|
+
|
19
|
+
attr_reader :timestamp, :msg_id, :source_id
|
20
|
+
|
21
|
+
def initialize(source)
|
22
|
+
raise MissingSource, "Source not provided." if source.nil?
|
23
|
+
@timestamp = Time.now
|
24
|
+
@msg_id = UUID.generate(:compact).upcase
|
25
|
+
@source_id = source
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Siffer
|
2
|
+
module Messages
|
3
|
+
class Register < Message
|
4
|
+
|
5
|
+
attr_reader :max_buffer_size, :name, :mode, :protocol, :application, :icon, :version, :node_vendor, :node_version
|
6
|
+
|
7
|
+
def initialize(source, *args)
|
8
|
+
super(source)
|
9
|
+
@options = args.extract_options!
|
10
|
+
@name = @options[:name]
|
11
|
+
@mode = @options[:mode] ||= "Pull"
|
12
|
+
raise NoProtocol, "Protocol not provided." if @mode == "Push" and @options[:protocol].nil?
|
13
|
+
@protocol = @options[:protocol]
|
14
|
+
@max_buffer_size = @options[:max_buffer_size] ||= 524288
|
15
|
+
@application = Application.new(@options[:application]) unless @options[:application].nil?
|
16
|
+
@icon = @options[:icon] unless @options[:icon].nil?
|
17
|
+
# This is the SIF version of messages that the Agent is capable of receiving/sending
|
18
|
+
# as opposed to the Message#version which is the version of h3o SIF implemented.
|
19
|
+
@version = @options[:version] ||= "2.*"
|
20
|
+
@node_vendor = Siffer::VENDOR
|
21
|
+
# The version of the registering Agent is the version of the SIF framework. This should always coordinate.
|
22
|
+
@node_version = Siffer::VERSION
|
23
|
+
end
|
24
|
+
|
25
|
+
# internal class used to model the application this registering Agent supports/represents
|
26
|
+
class Application
|
27
|
+
|
28
|
+
attr_reader :vendor, :product, :version
|
29
|
+
|
30
|
+
def initialize(options)
|
31
|
+
@vendor, @product, @version = options[:vendor], options[:product], options[:version]
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Siffer
|
2
|
+
module Protocols
|
3
|
+
class Protocol
|
4
|
+
|
5
|
+
attr_reader :url, :properties
|
6
|
+
|
7
|
+
def initialize(url,*properties)
|
8
|
+
raise MissingUrl, "Url not provided." if url.nil?
|
9
|
+
@url = url
|
10
|
+
@properties = properties.extract_options!
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/sif/status.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Siffer
|
2
|
+
class Status
|
3
|
+
|
4
|
+
attr_reader :code, :description, :data
|
5
|
+
|
6
|
+
def initialize(code, data = nil)
|
7
|
+
@code = H3O::SIF::CodeSets::StatusCode[code]
|
8
|
+
@description = @code.description
|
9
|
+
@data = data unless data.nil?
|
10
|
+
end
|
11
|
+
|
12
|
+
def ==(object)
|
13
|
+
return @code == object
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
data/lib/siffer.rb
CHANGED
@@ -1,41 +1 @@
|
|
1
|
-
require '
|
2
|
-
require 'activesupport'
|
3
|
-
require 'uuid'
|
4
|
-
require 'builder'
|
5
|
-
require 'sinatra/base'
|
6
|
-
require 'haml'
|
7
|
-
require 'rest_client'
|
8
|
-
require 'acdc'
|
9
|
-
|
10
|
-
$: << File.expand_path(File.dirname(__FILE__))
|
11
|
-
|
12
|
-
module Siffer
|
13
|
-
|
14
|
-
VENDOR = "h3o(software)" unless defined?(Siffer::VENDOR)
|
15
|
-
VERSION = [0,0,7] unless defined?(Siffer::VERSION)
|
16
|
-
SIF_VERSION = [2,3] unless defined?(Siffer::SIF_VERSION)
|
17
|
-
SIF_XMLNS = "http://www.sifinfo.org/infrastructure/2.x" unless defined?(Siffer::SIF_XMLNS)
|
18
|
-
|
19
|
-
# The vendor of this SIF implementation (self describing for Agents)
|
20
|
-
def self.vendor() VENDOR end
|
21
|
-
|
22
|
-
# The version of the h3o(software) SIF implementation
|
23
|
-
def self.version() VERSION.join(".") end
|
24
|
-
|
25
|
-
# The version of SIF being implemented - based on the specification
|
26
|
-
def self.sif_version() SIF_VERSION.join(".") end
|
27
|
-
|
28
|
-
# The SIF XML namespace to be used across this implementation
|
29
|
-
def self.sif_xmlns() SIF_XMLNS end
|
30
|
-
|
31
|
-
# The root directory that the SIF implementation is running from
|
32
|
-
def self.root() @root ||= Dir.pwd end
|
33
|
-
def self.root=(value) @root = value end
|
34
|
-
|
35
|
-
autoload :Messages, "siffer/messages"
|
36
|
-
autoload :Models, "siffer/models"
|
37
|
-
|
38
|
-
end
|
39
|
-
|
40
|
-
require "siffer/agent"
|
41
|
-
require "siffer/core_ext/hash"
|
1
|
+
require 'sif'
|