smartfocus 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8eac52d54c0b68c2544736e196208f5197772123
4
+ data.tar.gz: d84d9da100cae9d4ed3fd9b7b0dc987d58004c83
5
+ SHA512:
6
+ metadata.gz: 45ae22b51f28b0f7e39684f7ff170535d8ca4a41e2044c9eb51ee12e6063a7ced9e037a0eb1b9912428f41ba5a4eb54be1fc13e88238c4cff257ff97c8b473b4
7
+ data.tar.gz: d0c8255336649508e0a83e3156810b9e01030cb3fc1150a885e38e5f127ee6948f0a2a1b29a79d576f844c914a048853ea408aa529e6246ed8cd63e0f8fb6e04
@@ -0,0 +1,15 @@
1
+ require 'rails/generators'
2
+
3
+ module Smartfocus
4
+ module Generators
5
+ class Install < Rails::Generators::Base
6
+
7
+ source_root File.expand_path('../templates', __FILE__)
8
+
9
+ def generate_config
10
+ copy_file "smartfocus.yml", "config/smartfocus.yml" unless File.exist?("config/smartfocus.yml")
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ development:
2
+ server_name:
3
+ login:
4
+ password:
5
+ key:
6
+ production:
7
+ server_name:
8
+ login:
9
+ password:
10
+ key:
11
+ test:
12
+ server_name:
13
+ login:
14
+ password:
15
+ key:
data/lib/smartfocus.rb ADDED
@@ -0,0 +1,27 @@
1
+ require 'crack/xml'
2
+ require 'httparty'
3
+ require 'active_support/inflector'
4
+ require 'logger'
5
+ require 'builder'
6
+
7
+ # Smartfocus API wrapper
8
+ #
9
+ module Smartfocus
10
+ autoload :Api, 'smartfocus/api'
11
+ autoload :Exception, 'smartfocus/exception'
12
+ autoload :Logger, 'smartfocus/logger'
13
+ autoload :MalformedResponse, 'smartfocus/malformed_response'
14
+ autoload :Relation, 'smartfocus/relation'
15
+ autoload :Request, 'smartfocus/request'
16
+ autoload :RequestError, 'smartfocus/request_error'
17
+ autoload :Response, 'smartfocus/response'
18
+ autoload :SessionError, 'smartfocus/session_error'
19
+ autoload :Tools, 'smartfocus/tools'
20
+ autoload :Notification, 'smartfocus/notification'
21
+ autoload :Version, 'smartfocus/version'
22
+
23
+ if defined?(Rails)
24
+ require 'smartfocus/railtie'
25
+ require 'generators/install'
26
+ end
27
+ end
@@ -0,0 +1,173 @@
1
+ module Smartfocus
2
+
3
+ # This is where the communication with the API is made.
4
+ #
5
+ class Api
6
+ include HTTParty
7
+ default_timeout 30
8
+ format :xml
9
+ headers 'Content-Type' => 'text/xml'
10
+
11
+ # HTTP verbs allowed to trigger a call-chain
12
+ HTTP_VERBS = [:get, :post].freeze
13
+ ATTRIBUTES = [:token, :server_name, :endpoint, :login, :password, :key, :debug].freeze
14
+
15
+ # Attributes
16
+ class << self
17
+ attr_accessor *ATTRIBUTES
18
+ end
19
+ attr_accessor *ATTRIBUTES
20
+
21
+ # Initialize
22
+ #
23
+ # @param [Hash] Instance attributes to assign
24
+ # @yield Freshly-created instance (optionnal)
25
+ #
26
+ def initialize(params = {})
27
+ yield(self) if block_given?
28
+ assign_attributes(params)
29
+ end
30
+
31
+ # ----------------- BEGIN Pre-configured methods -----------------
32
+
33
+ # Reset session
34
+ #
35
+ # Useful when the session has expired.
36
+ #
37
+ def reset_session
38
+ close_connection
39
+ open_connection
40
+ end
41
+
42
+ # Login to Smartfocus API
43
+ #
44
+ # @return [Boolean] true if the connection has been established.
45
+ #
46
+ def open_connection
47
+ return false if connected?
48
+ self.token = get.connect.open.call :login => @login, :password => @password, :key => @key
49
+ connected?
50
+ end
51
+
52
+ # Logout from Smartfocus API
53
+ #
54
+ # @return [Boolean] true if the connection has been destroyed
55
+ #
56
+ def close_connection
57
+ if connected?
58
+ get.connect.close.call
59
+ else
60
+ return false
61
+ end
62
+ rescue Smartfocus::Exception => e
63
+ ensure
64
+ invalidate_token!
65
+ not connected?
66
+ end
67
+
68
+ # Check whether the connection has been established or not
69
+ #
70
+ # @return [Boolean] true if the connection has been establshed
71
+ #
72
+ def connected?
73
+ !token.nil?
74
+ end
75
+
76
+ # When a token is no longer valid, this method can be called.
77
+ # The #connected? method will return false
78
+ #
79
+ def invalidate_token!
80
+ self.token = nil
81
+ end
82
+ # ----------------- END Pre-configured methods -----------------
83
+
84
+ # Perform an API call
85
+ #
86
+ # @param [Smartfocus::Request] Request to perform
87
+ #
88
+ def call(request)
89
+ # == Check presence of these essential attributes ==
90
+ unless server_name and endpoint
91
+ raise Smartfocus::Exception, "Cannot make an API call without a server name and an endpoint !"
92
+ end
93
+
94
+ with_retries do
95
+ logger.send "#{request.uri} with query : #{request.parameters} and body : #{request.body}"
96
+
97
+ response = perform_request(request)
98
+
99
+ Smartfocus::Response.new(response, logger).extract
100
+ end
101
+ end
102
+
103
+ # Set a new token
104
+ #
105
+ # @param [String] new token
106
+ #
107
+ def token=(value)
108
+ @token = value
109
+ end
110
+
111
+ # Change API endpoint.
112
+ # This will close the connection to the current endpoint
113
+ #
114
+ # @param [String] new endpoint (apimember, apiccmd, apitransactional, ...)
115
+ #
116
+ def endpoint=(value)
117
+ close_connection
118
+ @endpoint = value
119
+ end
120
+
121
+ # Base uri
122
+ #
123
+ def base_uri
124
+ "http://#{server_name}/#{endpoint}/services/rest/"
125
+ end
126
+
127
+ # Generate call-chain triggers
128
+ HTTP_VERBS.each do |http_verb|
129
+ define_method(http_verb) do
130
+ Smartfocus::Relation.new(self, build_request(http_verb))
131
+ end
132
+ end
133
+
134
+ private
135
+
136
+ def build_request(http_verb)
137
+ Smartfocus::Request.new(http_verb, token, server_name, endpoint)
138
+ end
139
+
140
+ def perform_request(request)
141
+ self.class.send request.http_verb, base_uri + request.uri, :query => request.parameters, :body => request.body, :timeout => 30
142
+ end
143
+
144
+ def assign_attributes(parameters)
145
+ parameters or return
146
+ ATTRIBUTES.each do |attribute|
147
+ public_send("#{attribute}=", (parameters[attribute] || self.class.public_send(attribute)))
148
+ end
149
+ end
150
+
151
+ def with_retries
152
+ retries = 3
153
+ begin
154
+ yield
155
+ rescue Errno::ECONNRESET, Timeout::Error => e
156
+ if ((retries -= 1) > 0)
157
+ retry
158
+ else
159
+ raise e
160
+ end
161
+ end
162
+ end
163
+
164
+ def logger
165
+ if @logger.nil?
166
+ @logger = Smartfocus::Logger.new(STDOUT)
167
+ @logger.level = (debug ? Logger::DEBUG : Logger::WARN)
168
+ end
169
+ @logger
170
+ end
171
+
172
+ end
173
+ end
@@ -0,0 +1,8 @@
1
+ module Smartfocus
2
+
3
+ # API default exception
4
+ #
5
+ class Exception < ::StandardError
6
+
7
+ end
8
+ end
@@ -0,0 +1,29 @@
1
+ module Smartfocus
2
+
3
+ # API logger class
4
+ #
5
+ class Logger < ::Logger
6
+
7
+ attr_accessor :debug
8
+
9
+ def initializer(*args)
10
+ debug = false
11
+ super args
12
+ end
13
+
14
+ # Log a message sent to smartfocus
15
+ #
16
+ # @param [String] message
17
+ def send(message)
18
+ info("[Smartfocus] Send -> #{message}")
19
+ end
20
+
21
+ # Log a message received from smartfocus
22
+ #
23
+ # @param [String] message
24
+ def receive(message)
25
+ info("[Smartfocus] Receive -> #{message}")
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,10 @@
1
+ module Smartfocus
2
+
3
+ # Malformed response
4
+ #
5
+ # This error is raised when the response from Smartfocus
6
+ # cannot be parsed.
7
+ class MalformedResponse < Exception
8
+ end
9
+
10
+ end
@@ -0,0 +1,48 @@
1
+ module Smartfocus
2
+ class Notification
3
+ include HTTParty
4
+ default_timeout 30
5
+ format :xml
6
+ headers 'Content-Type' => 'application/xml;charset=utf-8', 'encoding' => 'UTF-8', 'Accept' => '*/*'
7
+
8
+ class << self
9
+ attr_accessor :debug
10
+ end
11
+ attr_accessor :debug
12
+
13
+ def initialize(params = {})
14
+ yield(self) if block_given?
15
+
16
+ self.debug ||= params[:debug] || self.class.debug
17
+ end
18
+
19
+ def send(body, params = {})
20
+ # == Processing body ==
21
+ body_xml = Smartocus::Tools.to_xml_as_is body
22
+
23
+ # == Send request ==
24
+ logger.send "#{base_uri} with body : #{body_xml}"
25
+ response = self.class.send :post, base_uri, :body => body_xml
26
+ logger.receive "#{base_uri} with status : #{response.header.inspect}"
27
+
28
+ # == Check result ==
29
+ response.header.code == '200'
30
+ end
31
+
32
+ # Base uri
33
+ def base_uri
34
+ 'http://api.notificationmessaging.com/NMSXML'
35
+ end
36
+
37
+ private
38
+
39
+ def logger
40
+ if @logger.nil?
41
+ @logger = Smartocus::Logger.new(STDOUT)
42
+ @logger.level = (debug ? Logger::DEBUG : Logger::WARN)
43
+ end
44
+ @logger
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,24 @@
1
+ require 'rails'
2
+
3
+ module Smartocus
4
+ class Railtie < Rails::Railtie
5
+
6
+ generators do
7
+ require 'generators/install'
8
+ end
9
+
10
+ config.to_prepare do
11
+ file = "#{Rails.root}/config/smartfocus.yml"
12
+ if File.exist?(file)
13
+ config = YAML.load_file(file)[Rails.env] || {}
14
+ Smartfocus::Api.server_name = config['server_name']
15
+ Smartfocus::Api.endpoint = config['endpoint']
16
+ Smartfocus::Api.login = config['login']
17
+ Smartfocus::Api.password = config['password']
18
+ Smartfocus::Api.key = config['key']
19
+ Smartfocus::Api.debug = config['debug']
20
+ Smartfocus::Notification.debug = config['debug']
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,40 @@
1
+ module Smartfocus
2
+
3
+ # Relation is used for API-chained call
4
+ #
5
+ # e.g. emv.get.campaign.last(:limit => 5).call
6
+ #
7
+ class Relation
8
+
9
+ def initialize(instance, request)
10
+ @instance = instance
11
+ @request = request
12
+ @uri = []
13
+ @options = {}
14
+ end
15
+
16
+ # Trigger the API call
17
+ #
18
+ # @param [Object] parameters
19
+ # @return [Object] data returned from Smartfocus
20
+ #
21
+ def call(*args)
22
+ @options.merge! extract_args(args)
23
+ @request.prepare(@uri.join('/'), @options)
24
+ @instance.call(@request)
25
+ end
26
+
27
+ def method_missing(method, *args)
28
+ @uri << method.to_s.camelize(:lower)
29
+ @options.merge! extract_args(args)
30
+ self
31
+ end
32
+
33
+ private
34
+
35
+ def extract_args(args)
36
+ (args[0] and args[0].kind_of? Hash) ? args[0] : {}
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,67 @@
1
+ module Smartfocus
2
+
3
+ # Request object
4
+ #
5
+ # This class aims to format the request for Smartfocus
6
+ #
7
+ class Request
8
+
9
+ attr_reader(
10
+ :http_verb,
11
+ :token,
12
+ :server_name,
13
+ :endpoint,
14
+ :uri,
15
+ :body,
16
+ :parameters
17
+ )
18
+
19
+ def initialize(http_verb, token, server_name, endpoint)
20
+ @http_verb = http_verb
21
+ @token = token
22
+ @server_name = server_name
23
+ @endpoint = endpoint
24
+ end
25
+
26
+ def prepare(uri, parameters)
27
+ @uri = uri
28
+ @parameters = parameters || {}
29
+
30
+ @uri = prepare_uri
31
+ @body = prepare_body
32
+ end
33
+
34
+ private
35
+
36
+ def prepare_uri
37
+ uri = @uri
38
+ if parameters[:uri]
39
+ uri += token ? "/#{token}/" : '/'
40
+ uri += (parameters[:uri].respond_to?(:join) ? parameters[:uri] : [parameters[:uri]]).compact.join '/'
41
+ parameters.delete :uri
42
+ elsif parameters[:body]
43
+ uri += token ? "/#{token}/" : '/'
44
+ else
45
+ parameters[:token] = token
46
+ end
47
+ uri
48
+ end
49
+
50
+ def prepare_body
51
+ body = parameters[:body] || {}
52
+ parameters.delete :body
53
+ # 2. Camelize all keys
54
+ body = Smartfocus::Tools.r_camelize body
55
+ # 3. Convert to xml
56
+ Smartfocus::Tools.to_xml_as_is body
57
+ end
58
+
59
+ def assign_attributes(attibutes)
60
+ attibutes or return
61
+ attibutes.each do |attribute, value|
62
+ public_send("#{attribute}=", value)
63
+ end
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,11 @@
1
+ module Smartfocus
2
+
3
+ # Malformed response
4
+ #
5
+ # This error is raised when the response from Smartfocus
6
+ # cannot be parsed.
7
+ class RequestError < Exception
8
+
9
+ end
10
+
11
+ end
@@ -0,0 +1,52 @@
1
+ module Smartfocus
2
+
3
+ # Response object
4
+ #
5
+ # This class aims to extract the response from Smartfocus
6
+ #
7
+ class Response
8
+
9
+ attr_reader :response, :logger
10
+
11
+ def initialize(response, logger)
12
+ @response = response
13
+ @logger = logger
14
+ end
15
+
16
+ def extract
17
+ logger.receive(content.inspect)
18
+
19
+ if succeed?
20
+ response = content["response"]["result"] || content["response"]
21
+ else
22
+ handle_errors
23
+ end
24
+ rescue MultiXml::ParseError, REXML::ParseException => error
25
+ wrapped_error = Smartfocus::MalformedResponse.new(error)
26
+ raise wrapped_error, "Error when parsing response body"
27
+ end
28
+
29
+ private
30
+
31
+ def succeed?
32
+ (http_code == "200") and (content and content["response"])
33
+ end
34
+
35
+ def handle_errors
36
+ if content =~ /Your session has expired/ or content =~ /The maximum number of connection allowed per session has been reached/
37
+ raise Smartfocus::SessionError, content
38
+ else
39
+ raise Smartfocus::RequestError, content
40
+ end
41
+ end
42
+
43
+ def content
44
+ @content ||= Crack::XML.parse(response.body)
45
+ end
46
+
47
+ def http_code
48
+ response.header.code
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,10 @@
1
+ module Smartfocus
2
+
3
+ # Session Error
4
+ #
5
+ # This error is raised when the token has expired
6
+ # or the number of connections has been reached
7
+ class SessionError < Exception
8
+ end
9
+
10
+ end
@@ -0,0 +1,130 @@
1
+ module Smartfocus
2
+
3
+ # Toolbox for the API
4
+ # This class is mainly used to convert data
5
+ #
6
+ class Tools
7
+
8
+ # Sanitize values from a Hash
9
+ #
10
+ # @param [Hash] hash to sanitize
11
+ # @return [Hash] sanitized hash
12
+ #
13
+ def self.sanitize_parameters(parameters)
14
+ r_each(parameters) do |value|
15
+ if value.kind_of?(DateTime) or value.kind_of?(Time)
16
+ date_time_format(value.to_datetime)
17
+ elsif value.kind_of?(Date)
18
+ date_format(value.to_date)
19
+ else
20
+ value
21
+ end
22
+ end
23
+ end
24
+
25
+ def self.date_time_format(datetime)
26
+ datetime.strftime("%Y-%m-%dT%H:%M:%S")
27
+ end
28
+
29
+ def self.date_format(date)
30
+ date.strftime('%d/%m/%Y')
31
+ end
32
+
33
+ # Convert hash keys to camel case
34
+ #
35
+ # @param [Object] structure to camelize
36
+ # @return [Object] structure with keys camelized (if any)
37
+ #
38
+ def self.r_camelize(obj)
39
+ if obj.is_a?(Hash)
40
+ new_obj = {}
41
+ obj.each do |key, value|
42
+ new_obj[key.to_s.camelize(:lower).to_sym] = r_camelize value
43
+ end
44
+ new_obj
45
+ elsif obj.is_a?(Array)
46
+ new_obj = []
47
+ obj.each_with_index do |item, index|
48
+ new_obj[index] = r_camelize item
49
+ end
50
+ new_obj
51
+ else
52
+ obj
53
+ end
54
+ end
55
+
56
+ # Convert data structure to XML
57
+ #
58
+ # @param [Object] structure to convert
59
+ # @return [String] XML structure
60
+ #
61
+ def self.to_xml_as_is(obj)
62
+ obj_xml = ""
63
+
64
+ unless obj.nil? or obj.empty?
65
+ xml = ::Builder::XmlMarkup.new(:target=> obj_xml)
66
+ xml.instruct! :xml, :version=> "1.0"
67
+ tag_obj xml, obj
68
+ end
69
+
70
+ obj_xml
71
+ end
72
+
73
+ # Iterate throught a Hash recursively
74
+ #
75
+ # @param [Hash] structure to iterate
76
+ # @yield called for each data
77
+ #
78
+ def self.r_each(hash, &block)
79
+ return enum_for(:dfs, hash) unless block
80
+
81
+ result = {}
82
+ if hash.is_a?(Hash)
83
+ hash.map do |k,v|
84
+ result[k] = if v.is_a? Array
85
+ v.map do |elm|
86
+ r_each(elm, &block)
87
+ end
88
+ elsif v.is_a? Hash
89
+ r_each(v, &block)
90
+ else
91
+ yield(v)
92
+ end
93
+ end
94
+ else
95
+ result = yield(hash)
96
+ end
97
+
98
+ result
99
+ end
100
+
101
+ private
102
+
103
+ def self.tag_obj(xml, obj)
104
+ if obj.is_a? Hash
105
+ obj.each do |key, value|
106
+ if value.is_a?(Hash)
107
+ eval(%{
108
+ xml.#{key} do
109
+ tag_obj(xml, value)
110
+ end
111
+ })
112
+ elsif value.is_a?(Array)
113
+ value.each do |item|
114
+ eval(%{
115
+ xml.#{key} do
116
+ tag_obj(xml, item)
117
+ end
118
+ })
119
+ end
120
+ else
121
+ eval %{xml.#{key}(%{#{value}})}
122
+ end
123
+ end
124
+ else
125
+ obj
126
+ end
127
+ end
128
+
129
+ end
130
+ end
@@ -0,0 +1,3 @@
1
+ module Smartfocus
2
+ Version = VERSION = '1.0.0'
3
+ end
metadata ADDED
@@ -0,0 +1,126 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: smartfocus
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - '''Bastien'
8
+ - Gysler',
9
+ - '''eboutic.ch'''
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2013-11-14 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: httparty
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.12.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ version: 0.12.0
29
+ - !ruby/object:Gem::Dependency
30
+ name: crack
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ~>
34
+ - !ruby/object:Gem::Version
35
+ version: 0.4.0
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ~>
41
+ - !ruby/object:Gem::Version
42
+ version: 0.4.0
43
+ - !ruby/object:Gem::Dependency
44
+ name: builder
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - '>='
48
+ - !ruby/object:Gem::Version
49
+ version: '3.0'
50
+ type: :runtime
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - '>='
55
+ - !ruby/object:Gem::Version
56
+ version: '3.0'
57
+ - !ruby/object:Gem::Dependency
58
+ name: activesupport
59
+ requirement: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '3.0'
64
+ - - ~>
65
+ - !ruby/object:Gem::Version
66
+ version: 4.0.0
67
+ type: :runtime
68
+ prerelease: false
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - '>='
72
+ - !ruby/object:Gem::Version
73
+ version: '3.0'
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ version: 4.0.0
77
+ description: REST API wrapper interacting with Smartfocus (ex Emailvision)
78
+ email:
79
+ - '''basgys@gmail.com'','
80
+ - '''tech@eboutic.ch'''
81
+ executables: []
82
+ extensions: []
83
+ extra_rdoc_files: []
84
+ files:
85
+ - lib/generators/install.rb
86
+ - lib/generators/templates/smartfocus.yml
87
+ - lib/smartfocus/api.rb
88
+ - lib/smartfocus/exception.rb
89
+ - lib/smartfocus/logger.rb
90
+ - lib/smartfocus/malformed_response.rb
91
+ - lib/smartfocus/notification.rb
92
+ - lib/smartfocus/railtie.rb
93
+ - lib/smartfocus/relation.rb
94
+ - lib/smartfocus/request.rb
95
+ - lib/smartfocus/request_error.rb
96
+ - lib/smartfocus/response.rb
97
+ - lib/smartfocus/session_error.rb
98
+ - lib/smartfocus/tools.rb
99
+ - lib/smartfocus/version.rb
100
+ - lib/smartfocus.rb
101
+ homepage: https://github.com/eboutic/smartfocus
102
+ licenses:
103
+ - MIT
104
+ metadata: {}
105
+ post_install_message:
106
+ rdoc_options: []
107
+ require_paths:
108
+ - lib
109
+ required_ruby_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - '>='
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ required_rubygems_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - '>='
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ requirements: []
120
+ rubyforge_project:
121
+ rubygems_version: 2.1.9
122
+ signing_key:
123
+ specification_version: 4
124
+ summary: Smartfocus
125
+ test_files: []
126
+ has_rdoc: