evva 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.
@@ -0,0 +1,24 @@
1
+ require 'fileutils'
2
+
3
+ module Evva
4
+ class FileReader
5
+ def open_file(file_name, method, should_exist)
6
+ unless File.file?(File.expand_path(file_name))
7
+ if should_exist
8
+ Logger.error("File #{file_name} not found!")
9
+ return nil
10
+ else
11
+ FileUtils.mkdir_p(File.dirname(file_name))
12
+ end
13
+ end
14
+
15
+ File.open(File.expand_path(file_name), method)
16
+ end
17
+
18
+ def write_to_file(file, data)
19
+ file.write(data)
20
+ file.flush
21
+ file.close
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,91 @@
1
+ require 'net/https'
2
+ require 'xmlsimple'
3
+
4
+ module Evva
5
+ class GoogleSheet
6
+ def initialize(sheet_id)
7
+ @sheet_id = sheet_id
8
+ end
9
+
10
+ def events
11
+ raw = raw_data(@sheet_id, 0)
12
+ Logger.info('Downloading dictionary from Google Sheet...')
13
+ non_language_columns = %w[id updated category
14
+ title content link]
15
+ event_list = []
16
+ raw['entry'].each do |entry|
17
+ filtered_entry = entry.reject { |c| non_language_columns.include?(c) }
18
+ event_name = filtered_entry['eventname'].first
19
+ properties = hash_parser(filtered_entry['props'].first)
20
+ event_list.push(Evva::MixpanelEvent.new(event_name, properties))
21
+ end
22
+ event_list
23
+ end
24
+
25
+ def people_properties
26
+ raw = raw_data(@sheet_id, 1)
27
+ people_list = []
28
+ Logger.info('Downloading dictionary from Google Sheet...')
29
+ non_language_columns = %w[id updated category title content link]
30
+ raw['entry'].each do |entry|
31
+ filtered_entry = entry.reject { |c| non_language_columns.include?(c) }
32
+ value = filtered_entry['value'].first
33
+ people_list << value
34
+ end
35
+ people_list
36
+ end
37
+
38
+ def enum_classes
39
+ raw = raw_data(@sheet_id, 2)
40
+ Logger.info('Downloading dictionary from Google Sheet...')
41
+ non_language_columns = %w[id updated category title content link]
42
+ enum_list = []
43
+ raw['entry'].each do |entry|
44
+ filtered_entry = entry.reject { |c| non_language_columns.include?(c) }
45
+ enum_name = filtered_entry['enum'].first
46
+ values = filtered_entry['values'].first.split(',')
47
+ enum_list.push(Evva::MixpanelEnum.new(enum_name, values))
48
+ end
49
+ enum_list
50
+ end
51
+
52
+ def xml_data(uri, headers = nil)
53
+ uri = URI.parse(uri)
54
+ http = Net::HTTP.new(uri.host, uri.port)
55
+ http.use_ssl = true
56
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
57
+ data = http.get(uri.path, headers)
58
+ unless data.code.to_i == 200
59
+ raise 'Cannot access sheet at #{uri} - HTTP #{data.code}'
60
+ end
61
+
62
+ begin
63
+ XmlSimple.xml_in(data.body, 'KeyAttr' => 'name')
64
+ rescue
65
+ raise 'Cannot parse. Expected XML at #{uri}'
66
+ end
67
+ end
68
+
69
+ def raw_data(sheet_id, sheet_number)
70
+ Logger.info('Downloading Google Sheet...')
71
+ sheet = xml_data("https://spreadsheets.google.com/feeds/worksheets/#{sheet_id}/public/full")
72
+ url = sheet['entry'][sheet_number]['link'][0]['href']
73
+ xml_data(url)
74
+ end
75
+
76
+ private
77
+
78
+ def hash_parser(property_array)
79
+ h = {}
80
+ unless property_array.empty?
81
+ property_array.split(',').each do |prop|
82
+ split_prop = prop.split(':')
83
+ prop_name = split_prop[0].to_sym
84
+ prop_type = split_prop[1].to_s
85
+ h[prop_name] = prop_type
86
+ end
87
+ end
88
+ h
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,61 @@
1
+ require 'singleton'
2
+ require 'colorize'
3
+
4
+ module Evva
5
+ module Logger
6
+ extend self
7
+
8
+ def info(msg)
9
+ log :info, msg
10
+ end
11
+
12
+ def warn(msg)
13
+ log :warn, msg
14
+ end
15
+
16
+ def error(msg)
17
+ log :error, msg
18
+ end
19
+
20
+ def clean_summary
21
+ @levels.each { |k, _| @levels[k] = 0 }
22
+ end
23
+
24
+ def summary
25
+ @levels
26
+ end
27
+
28
+ def print_summary
29
+ if @levels[:warn] > 0 || @levels[:error] > 0
30
+ info ''
31
+ info 'Finished with:'
32
+ info " #{@levels[:warn]} warnings" if @levels[:warn] > 0
33
+ info " #{@levels[:error]} errors" if @levels[:error] > 0
34
+ info ''
35
+ end
36
+ end
37
+
38
+ def silent_mode=(value)
39
+ @silent_mode = value
40
+ end
41
+
42
+ private
43
+
44
+ @levels = { info: 0, warn: 0, error: 0 }
45
+ @silent_mode = false
46
+
47
+ def log(level, msg)
48
+ unless @levels.keys.include?(level)
49
+ return log(:error, "Unknown log level: #{level}")
50
+ end
51
+
52
+ @levels[level] += 1
53
+
54
+ msg = "[#{level.upcase}] #{msg}"
55
+ msg = msg.yellow if level.eql?(:warn)
56
+ msg = msg.red if level.eql?(:error)
57
+
58
+ puts msg unless @silent_mode
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,14 @@
1
+ module Evva
2
+ class MixpanelEnum
3
+ attr_reader :enum_name, :values
4
+ def initialize(enum_name, values)
5
+ @enum_name = enum_name
6
+ @values = values
7
+ end
8
+
9
+ def ==(other)
10
+ enum_name == other.enum_name
11
+ values == other.values
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module Evva
2
+ class MixpanelEvent
3
+ attr_reader :event_name, :properties
4
+ def initialize(event_name, properties = {})
5
+ @event_name = event_name
6
+ @properties = properties
7
+ end
8
+
9
+ def ==(other)
10
+ event_name == other.event_name
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,41 @@
1
+ Object.class_eval do
2
+ def deep_symbolize
3
+ case self
4
+ when Array
5
+ map(&:deep_symbolize)
6
+ when Hash
7
+ each_with_object({}) { |(k, v), memo| memo[k.to_sym] = v.deep_symbolize; }
8
+ else
9
+ self
10
+ end
11
+ end
12
+
13
+ def validate_structure!(structure, error_prefix = [])
14
+ return if nil? && structure[:optional]
15
+
16
+ prepend_error = error_prefix.empty? ? '' : (['self'] + error_prefix + [': ']).join
17
+
18
+ unless is_a? structure[:type]
19
+ raise ArgumentError, "#{prepend_error}Expected #{structure[:type]}, got #{self.class}"
20
+ end
21
+
22
+ return unless structure[:elements]
23
+
24
+ case self
25
+ when Array
26
+ each_with_index do |e, i|
27
+ e.validate_structure!(structure[:elements], error_prefix + ["[#{i}]"])
28
+ end
29
+ when Hash
30
+ mandatory_keys = structure[:elements].map { |k, s| k unless s[:optional] }.compact
31
+
32
+ unless (missing = mandatory_keys - keys).empty?
33
+ raise ArgumentError, "#{prepend_error}Missing keys: #{missing.join(', ')}"
34
+ end
35
+
36
+ structure[:elements].each do |key, structure|
37
+ self[key].validate_structure!(structure, error_prefix + ["[:#{key}]"])
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,128 @@
1
+ module Evva
2
+ class SwiftGenerator
3
+ SWIFT_EVENT_HEADER =
4
+ "import CoreLocation\n"\
5
+ "import Foundation\n"\
6
+ "import SharedCode\n\n"\
7
+ "class MixpanelHelper: NSObject {\n"\
8
+ "enum Event {\n".freeze
9
+
10
+ SWIFT_EVENT_DATA_HEADER =
11
+ "private var data: EventData {\n"\
12
+ "switch self {\n\n\n".freeze
13
+
14
+ SWIFT_PEOPLE_HEADER = "fileprivate enum Counter: String {\n".freeze
15
+
16
+ SWIFT_INCREMENT_FUNCTION =
17
+ "func increment(times: Int = 1) {\n"\
18
+ "MixpanelAPI.instance.incrementCounter(rawValue, times: times)\n"\
19
+ '}'.freeze
20
+
21
+ def events(bundle)
22
+ event_file = SWIFT_EVENT_HEADER
23
+ bundle.each do |event|
24
+ event_file += swift_case(event)
25
+ end
26
+ event_file += "}\n"
27
+ event_file += "private var data: EventData {\n"\
28
+ "switch self {\n\n"
29
+ bundle.each do |event|
30
+ event_file += swift_event_data(event)
31
+ end
32
+ event_file += "}\n}\n"
33
+ end
34
+
35
+ def swift_case(event_data)
36
+ function_name = 'track' + titleize(event_data.event_name)
37
+ if event_data.properties.nil?
38
+ case_body = "\t\tcase #{function_name}\n"
39
+ else
40
+ trimmed_properties = event_data.properties.gsub('Boolean', 'Bool')
41
+ case_body = "\t\tcase #{function_name}(#{trimmed_properties})\n"
42
+ end
43
+ end
44
+
45
+ def swift_event_data(event_data)
46
+ function_name = 'track' + titleize(event_data.event_name)
47
+ if event_data.properties.nil?
48
+ function_body = "case .#{function_name} \n" \
49
+ "\treturn EventData(name:" + %("#{event_data.event_name}") + ")\n\n"
50
+ else
51
+ function_header = prepend_let(event_data.properties)
52
+ function_arguments = process_arguments(event_data.properties.gsub('Boolean', 'Bool'))
53
+ function_body = "case .#{function_name}(#{function_header}):\n" \
54
+ "\treturn EventData(name:" + %("#{event_data.event_name}") + ", properties: [#{function_arguments}])\n\n"
55
+ end
56
+
57
+ function_body
58
+ end
59
+
60
+ def event_enum(enum)
61
+ # empty
62
+ end
63
+
64
+ def people_properties(people_bundle)
65
+ properties = SWIFT_PEOPLE_HEADER
66
+ people_bundle.each do |prop|
67
+ properties += swift_people_const(prop)
68
+ end
69
+ properties += SWIFT_INCREMENT_FUNCTION + "\n}"
70
+ end
71
+
72
+ def special_property_enum(enum)
73
+ enum_body = "import Foundation\n\n"
74
+ enum_values = enum.values.split(',')
75
+ enum_body += "enum #{enum.enum_name}: String {\n"
76
+ enum_values.each do |vals|
77
+ enum_body += "\tcase #{vals.tr(' ', '_')} = " + %("#{vals}") + "\n"
78
+ end
79
+ enum_body += "} \n"
80
+ end
81
+
82
+ def process_arguments(props)
83
+ arguments = ''
84
+ props.split(',').each do |property|
85
+ if is_special_property(property)
86
+ if is_optional_property(property)
87
+
88
+ else
89
+ arguments += %("#{property.split(':').first}") + ':' + property.split(':').first + '.rawValue, '
90
+ end
91
+ else
92
+ arguments += %("#{property.split(':').first}") + ':' + property.split(':').first + ', '
93
+ end
94
+ end
95
+ arguments.chomp(', ')
96
+ end
97
+
98
+ private
99
+
100
+ def is_special_property(prop)
101
+ types_array = %w[Long Int String Double Float Boolean]
102
+ type = prop.split(':')[1]
103
+ types_array.include?(type) ? false : true
104
+ end
105
+
106
+ def is_optional_property(prop)
107
+ type = prop.split(':')[1]
108
+ type.include?('?') ? true : false
109
+ end
110
+
111
+
112
+ def prepend_let(props)
113
+ function_header = ''
114
+ props.split(',').each do |property|
115
+ function_header += 'let ' + property.split(':')[0] + ', '
116
+ end
117
+ function_header.chomp(', ')
118
+ end
119
+
120
+ def swift_people_const(prop)
121
+ case_body = "\tcase #{prop.property_name} = " + %("#{prop.property_value}") + "\n"
122
+ end
123
+
124
+ def titleize(str)
125
+ str.split('_').collect(&:capitalize).join
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,4 @@
1
+ module Evva
2
+ VERSION = '0.1.0'.freeze
3
+ VERSION_UPDATED_AT = '2017-10-26'.freeze
4
+ end
@@ -0,0 +1,54 @@
1
+ Metrics/BlockNesting:
2
+ Max: 2
3
+
4
+ Metrics/LineLength:
5
+ AllowURI: true
6
+ Enabled: false
7
+
8
+ Metrics/MethodLength:
9
+ CountComments: false
10
+ Max: 15
11
+
12
+ Metrics/ParameterLists:
13
+ Max: 4
14
+ CountKeywordArgs: true
15
+
16
+ Style/AccessModifierIndentation:
17
+ EnforcedStyle: outdent
18
+
19
+ Style/CollectionMethods:
20
+ PreferredMethods:
21
+ map: 'collect'
22
+ reduce: 'inject'
23
+ find: 'detect'
24
+ find_all: 'select'
25
+
26
+ Style/Documentation:
27
+ Enabled: false
28
+
29
+ Style/DotPosition:
30
+ EnforcedStyle: trailing
31
+
32
+ Style/DoubleNegation:
33
+ Enabled: false
34
+
35
+ Style/EachWithObject:
36
+ Enabled: false
37
+
38
+ Style/Encoding:
39
+ Enabled: false
40
+
41
+ Style/HashSyntax:
42
+ EnforcedStyle: hash_rockets
43
+
44
+ Style/Lambda:
45
+ Enabled: false
46
+
47
+ Style/RaiseArgs:
48
+ EnforcedStyle: compact
49
+
50
+ Style/SpaceInsideHashLiteralBraces:
51
+ EnforcedStyle: no_space
52
+
53
+ Style/TrailingComma:
54
+ EnforcedStyleForMultiline: 'comma'
@@ -0,0 +1,40 @@
1
+ describe Evva do
2
+ subject(:run) { Evva.run([]) }
3
+
4
+ context "when there is a config.yml file" do
5
+ let(:file) { File.open("spec/fixtures/test.yml") }
6
+
7
+ before do
8
+ allow_any_instance_of(Evva::FileReader).to receive(:open_file).and_return(file)
9
+ allow_any_instance_of(Evva::GoogleSheet).to receive(:events).and_return(
10
+ [Evva::MixpanelEvent.new('trackEvent',[])])
11
+
12
+ allow_any_instance_of(Evva::GoogleSheet).to receive(:people_properties).and_return([])
13
+ allow_any_instance_of(Evva::GoogleSheet).to receive(:enum_classes).and_return([])
14
+
15
+ allow(Evva).to receive(:write_to_file)
16
+ end
17
+
18
+ it { expect { run }.not_to raise_error }
19
+
20
+ it "logs an error" do
21
+ expect {
22
+ run
23
+ }.to not_change { Evva::Logger.summary[:warn] }
24
+ .and not_change { Evva::Logger.summary[:error] }
25
+ end
26
+ end
27
+
28
+ context "when generic.yml does not exist locally" do
29
+ let(:error) { "Could not open yml file" }
30
+ before do
31
+ allow_any_instance_of(Evva::FileReader).to receive(:open_file).and_return(false)
32
+ end
33
+ it { expect { run }.to_not raise_error }
34
+
35
+ it "logs an error" do
36
+ expect { run }.to not_change { Evva::Logger.summary[:warn] }
37
+ .and change { Evva::Logger.summary[:error] }.by(1)
38
+ end
39
+ end
40
+ end