evva 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +5 -0
- data/.rubocop_todo.yml +477 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +79 -0
- data/README.md +27 -0
- data/bin/evva +4 -0
- data/evva.gemspec +24 -0
- data/evva_config.yml +10 -0
- data/lib/evva.rb +95 -0
- data/lib/evva/android_generator.rb +115 -0
- data/lib/evva/config.rb +68 -0
- data/lib/evva/data_source.rb +23 -0
- data/lib/evva/file_reader.rb +24 -0
- data/lib/evva/google_sheet.rb +91 -0
- data/lib/evva/logger.rb +61 -0
- data/lib/evva/mixpanel_enum.rb +14 -0
- data/lib/evva/mixpanel_event.rb +13 -0
- data/lib/evva/object_extension.rb +41 -0
- data/lib/evva/swift_generator.rb +128 -0
- data/lib/evva/version.rb +4 -0
- data/rubocop.yml +54 -0
- data/spec/evva_spec.rb +40 -0
- data/spec/fixtures/sample_public_enums.html +1 -0
- data/spec/fixtures/sample_public_info.html +1 -0
- data/spec/fixtures/sample_public_people_properties.html +1 -0
- data/spec/fixtures/sample_public_sheet.html +1 -0
- data/spec/fixtures/test.yml +10 -0
- data/spec/lib/evva/android_generator_spec.rb +135 -0
- data/spec/lib/evva/config_spec.rb +40 -0
- data/spec/lib/evva/data_source_spec.rb +28 -0
- data/spec/lib/evva/google_sheet_spec.rb +107 -0
- data/spec/lib/evva/logger_spec.rb +36 -0
- data/spec/lib/evva/object_extension_spec.rb +99 -0
- data/spec/lib/evva/swift_generator_spec.rb +81 -0
- data/spec/spec_helper.rb +113 -0
- metadata +136 -0
@@ -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
|
data/lib/evva/logger.rb
ADDED
@@ -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,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
|
data/lib/evva/version.rb
ADDED
data/rubocop.yml
ADDED
@@ -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'
|
data/spec/evva_spec.rb
ADDED
@@ -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
|