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.
- 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
|