evva 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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