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.
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source 'https://rubygems.org'
2
+ ruby '2.3.3'
3
+
4
+ gem 'colorize', '~> 0.7'
5
+ gem 'rubocop', '~> 0.49.1'
6
+ gem 'safe_yaml', '~> 1.0'
7
+ gem 'xml-simple', '~> 1.1'
8
+
9
+ gem 'rspec'
10
+ gem 'rspec-its'
11
+ gem 'simplecov', require: false, group: :test
12
+ gem 'simplecov-rcov', require: false
13
+ gem 'webmock', '~> 1.20'
@@ -0,0 +1,79 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ addressable (2.5.1)
5
+ public_suffix (~> 2.0, >= 2.0.2)
6
+ ast (2.3.0)
7
+ colorize (0.8.1)
8
+ crack (0.4.3)
9
+ safe_yaml (~> 1.0.0)
10
+ diff-lcs (1.3)
11
+ docile (1.1.5)
12
+ hashdiff (0.3.5)
13
+ json (2.1.0)
14
+ parallel (1.12.0)
15
+ parser (2.4.0.0)
16
+ ast (~> 2.2)
17
+ powerpack (0.1.1)
18
+ public_suffix (2.0.5)
19
+ rainbow (2.2.2)
20
+ rake
21
+ rake (12.0.0)
22
+ rspec (3.6.0)
23
+ rspec-core (~> 3.6.0)
24
+ rspec-expectations (~> 3.6.0)
25
+ rspec-mocks (~> 3.6.0)
26
+ rspec-core (3.6.0)
27
+ rspec-support (~> 3.6.0)
28
+ rspec-expectations (3.6.0)
29
+ diff-lcs (>= 1.2.0, < 2.0)
30
+ rspec-support (~> 3.6.0)
31
+ rspec-its (1.2.0)
32
+ rspec-core (>= 3.0.0)
33
+ rspec-expectations (>= 3.0.0)
34
+ rspec-mocks (3.6.0)
35
+ diff-lcs (>= 1.2.0, < 2.0)
36
+ rspec-support (~> 3.6.0)
37
+ rspec-support (3.6.0)
38
+ rubocop (0.49.1)
39
+ parallel (~> 1.10)
40
+ parser (>= 2.3.3.1, < 3.0)
41
+ powerpack (~> 0.1)
42
+ rainbow (>= 1.99.1, < 3.0)
43
+ ruby-progressbar (~> 1.7)
44
+ unicode-display_width (~> 1.0, >= 1.0.1)
45
+ ruby-progressbar (1.8.1)
46
+ safe_yaml (1.0.4)
47
+ simplecov (0.15.0)
48
+ docile (~> 1.1.0)
49
+ json (>= 1.8, < 3)
50
+ simplecov-html (~> 0.10.0)
51
+ simplecov-html (0.10.2)
52
+ simplecov-rcov (0.2.3)
53
+ simplecov (>= 0.4.1)
54
+ unicode-display_width (1.3.0)
55
+ webmock (1.24.6)
56
+ addressable (>= 2.3.6)
57
+ crack (>= 0.3.2)
58
+ hashdiff
59
+ xml-simple (1.1.5)
60
+
61
+ PLATFORMS
62
+ ruby
63
+
64
+ DEPENDENCIES
65
+ colorize (~> 0.7)
66
+ rspec
67
+ rspec-its
68
+ rubocop (~> 0.49.1)
69
+ safe_yaml (~> 1.0)
70
+ simplecov
71
+ simplecov-rcov
72
+ webmock (~> 1.20)
73
+ xml-simple (~> 1.1)
74
+
75
+ RUBY VERSION
76
+ ruby 2.3.3p222
77
+
78
+ BUNDLED WITH
79
+ 1.15.4
@@ -0,0 +1,27 @@
1
+ Evva
2
+
3
+ # Instalation
4
+
5
+ ` gem install evva `
6
+
7
+ # Usage
8
+ 1. Open the terminal in app project base
9
+ 2. Run `evva`
10
+ 3. That's it (given that someone already configured Evva)
11
+
12
+ # Configuration
13
+ Evva's configuration comes from a evva_config.yml file that should be placed on your
14
+ app root directory. The .yml file has the following structure.
15
+
16
+ ```
17
+ type: Android|iOS
18
+
19
+ data_source:
20
+ type: google_sheet
21
+ sheet_id: <GOOGLE-DRIVE-SHEET-ID>
22
+
23
+ out_path: /folder/where/analytics/classes/are
24
+ event_file_name: /file/with/tracking/functions
25
+ event_enum_file_name: /file/with/event/names
26
+ people_file_name: /file/with/people/properties
27
+ ```
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'evva'
4
+ Evva.run(ARGV)
@@ -0,0 +1,24 @@
1
+ require_relative 'lib/evva/version'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'evva'
5
+ s.version = Evva::VERSION
6
+ s.date = Evva::VERSION_UPDATED_AT
7
+ s.summary = 'An event generating service'
8
+ s.description = 'Evva generates all the analytics event tracking functions for you'
9
+ s.authors = ['RicardoTrindade']
10
+ s.email = 'ricardo.trindade743@gmail.com'
11
+ s.license = 'MIT'
12
+ s.homepage = 'https://github.com/hole19/'
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.require_paths = ['lib']
16
+
17
+ s.executables << 'evva'
18
+
19
+ s.add_runtime_dependency 'safe_yaml', '~> 1.0'
20
+ s.add_runtime_dependency 'colorize', '~> 0.7'
21
+ s.add_runtime_dependency 'xml-simple', '~> 1.1'
22
+
23
+ s.add_development_dependency 'webmock', '~> 1.20'
24
+ end
@@ -0,0 +1,10 @@
1
+ type: Android
2
+
3
+ data_source:
4
+ type: google_sheet
5
+ sheet_id: 1LaJd68os3g_GFlerogC64grNIlXb2iukMznOvdml7A4
6
+
7
+ out_path: analytics
8
+ event_file_name: MixpanelAnalytics
9
+ event_enum_file_name: MixpanelEvent
10
+ people_file_name: MixpanelProperties
@@ -0,0 +1,95 @@
1
+ require 'optparse'
2
+ require 'yaml'
3
+
4
+ require 'evva/logger'
5
+ require 'evva/google_sheet'
6
+ require 'evva/config'
7
+ require 'evva/file_reader'
8
+ require 'evva/data_source'
9
+ require 'evva/mixpanel_event'
10
+ require 'evva/mixpanel_enum'
11
+ require 'evva/object_extension'
12
+ require 'evva/version'
13
+ require 'evva/android_generator'
14
+ require 'evva/swift_generator'
15
+
16
+ module Evva
17
+ extend self
18
+ def run(options)
19
+ file_reader = Evva::FileReader.new
20
+ options = command_line_options(options)
21
+ unless config_file = file_reader.open_file('evva_config.yml', 'r', true)
22
+ Logger.error("Could not open evva_config.yml")
23
+ return
24
+ end
25
+
26
+ config = Evva::Config.new(hash: YAML.safe_load(config_file))
27
+ bundle = analytics_data(config: config.data_source)
28
+ case config.type.downcase
29
+ when 'android'
30
+ generator = Evva::AndroidGenerator.new
31
+ evva_write(bundle, generator, config, 'kt')
32
+ when 'ios'
33
+ generator = Evva::SwiftGenerator.new
34
+ evva_write(bundle, generator, config, 'swift')
35
+ end
36
+ Evva::Logger.print_summary
37
+ end
38
+
39
+ def evva_write(bundle, generator, configuration, extension)
40
+ path = "#{configuration.out_path}/#{configuration.event_file_name}.#{extension}"
41
+ write_to_file(path, generator.events(bundle[:events]))
42
+
43
+ path = "#{configuration.out_path}/#{configuration.event_enum_file_name}.#{extension}"
44
+ write_to_file(path, generator.event_enum(bundle[:events]))
45
+
46
+ path = "#{configuration.out_path}/#{configuration.people_file_name}.#{extension}"
47
+ write_to_file(path, generator.people_properties(bundle[:people]))
48
+
49
+ bundle[:enums].each do |enum|
50
+ path = "#{configuration.out_path}/#{enum.enum_name}.#{extension}"
51
+ write_to_file(path, generator.special_property_enum(enum))
52
+ end
53
+ end
54
+
55
+ def analytics_data(config:)
56
+ source =
57
+ case config[:type]
58
+ when 'google_sheet'
59
+ Evva::GoogleSheet.new(config[:sheet_id])
60
+ end
61
+ events_bundle = {}
62
+ events_bundle[:events] = source.events
63
+ events_bundle[:people] = source.people_properties
64
+ events_bundle[:enums] = source.enum_classes
65
+ events_bundle
66
+ end
67
+
68
+ def command_line_options(options)
69
+ opts_hash = {}
70
+
71
+ opts_parser = OptionParser.new do |opts|
72
+ opts.on_tail('-h', '--help', 'Show this message') do
73
+ puts opts
74
+ exit
75
+ end
76
+
77
+ opts.on_tail('-v', '--version', 'Show version') do
78
+ puts Evva::VERSION
79
+ exit
80
+ end
81
+ end
82
+ opts_parser.parse!(options)
83
+
84
+ opts_hash
85
+ end
86
+
87
+ def write_to_file(path, data)
88
+ file_reader = Evva::FileReader.new
89
+ if file = file_reader.open_file(path, "w", false)
90
+ file_reader.write_to_file(file, data)
91
+ else
92
+ Logger.error("Could not write to file in #{path}")
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,115 @@
1
+ module Evva
2
+ class AndroidGenerator
3
+ KOTLIN_EVENT_HEADER =
4
+ "package com.hole19golf.hole19.analytics\n\n"\
5
+ "import com.hole19golf.hole19.analytics.Event\n"\
6
+ "import com.hole19golf.hole19.analytics.MixpanelAnalyticsMask\n"\
7
+ "import org.json.JSONObject\n\n"\
8
+ "open class MixpanelEvents(private val mixpanelMask: MixpanelAnalyticsMask) {\n".freeze
9
+
10
+ KOTLIN_PEOPLE_HEADER =
11
+ "package com.hole19golf.hole19.analytics\n"\
12
+ "import com.hole19golf.hole19.analytics.Event\n\n"\
13
+ "enum class MixpanelProperties(val key: String) {\n".freeze
14
+
15
+ KOTLIN_BUNDLE_HEADER =
16
+ "package com.hole19golf.hole19.analytics\n"\
17
+ "import com.hole19golf.hole19.analytics.Event\n\n"\
18
+ "enum class MixpanelEvent(override val key: String) : Event {\n".freeze
19
+
20
+ KOTIN_PEOPLE_FUNCTIONS =
21
+ "\topen fun updateProperties(property: MixpanelProperties, value: Any) {\n"\
22
+ "\t\tmixpanelMask.updateProperties(property.key, value)"\
23
+ "\t\n} \n"\
24
+ "\topen fun incrementCounter(property: MixpanelProperties) {\n"\
25
+ "\t\tmixpanelMask.incrementCounter(property.key)"\
26
+ "\t\n} \n".freeze
27
+
28
+ NATIVE_TYPES = %w[Long Int String Double Float Boolean].freeze
29
+
30
+ def events(bundle)
31
+ event_file = KOTLIN_EVENT_HEADER
32
+ bundle.each do |event|
33
+ event_file += "\n#{kotlin_function(event)}"
34
+ end
35
+ event_file += KOTIN_PEOPLE_FUNCTIONS
36
+ event_file += "\n}"
37
+ end
38
+
39
+ def people_properties(people_bundle)
40
+ properties = KOTLIN_PEOPLE_HEADER
41
+ properties += people_bundle.map { |prop| "\t\t#{prop.upcase}(\"#{prop}\")" }.join(",\n")
42
+ properties += ";\n}\n"
43
+ end
44
+
45
+ def event_enum(bundle)
46
+ event_file = KOTLIN_BUNDLE_HEADER
47
+ event_file += bundle.map { |event| "\t\t#{event.event_name.upcase}(\"#{event.event_name}\")"}.join(", \n")
48
+ event_file += "\n}\n"
49
+ end
50
+
51
+ def kotlin_function(event_data)
52
+ function_name = 'track' + titleize(event_data.event_name)
53
+ function_arguments = event_data.properties.map { |name, type| "#{name}: #{type}" }.join(', ')
54
+ if !function_arguments.empty?
55
+ props = json_props(event_data.properties)
56
+ function_body =
57
+ "open fun #{function_name}(#{function_arguments}) {"\
58
+ "#{props}"\
59
+ "\tmixpanelMask.trackEvent(MixpanelEvent.#{event_data.event_name.upcase}, properties)\n"
60
+ else
61
+ function_body =
62
+ "open fun #{function_name}() {\n"\
63
+ "\tmixpanelMask.trackEvent(MixpanelEvent.#{event_data.event_name.upcase})\n"
64
+ end
65
+ function_body += "}\n"
66
+ end
67
+
68
+ def special_property_enum(enum)
69
+ enum_body = "package com.hole19golf.hole19.analytics\n\n"
70
+ enum_body += "enum class #{enum.enum_name}(val key: String) {\n"
71
+ enum_body += enum.values.map { |vals| "\t#{vals.tr(' ', '_').upcase}(\"#{vals}\")"}.join(",\n")
72
+ enum_body += "\n}\n"
73
+ end
74
+
75
+ private
76
+
77
+ def json_props(properties)
78
+ split_properties =
79
+ properties
80
+ .map do |name, type|
81
+ if special_property?(type)
82
+ if optional_property?(type)
83
+ "#{name}?.let { put(\"#{name}\", it.key) }"
84
+ else
85
+ "put(\"#{name}\", #{name}.key)"
86
+ end
87
+ else
88
+ if optional_property?(type)
89
+ "#{name}?.let { put(\"#{name}\", it) }"
90
+ else
91
+ "put(\"#{name}\", #{name})"
92
+ end
93
+ end
94
+ end
95
+ .map { |line| "\t\t#{line}" }
96
+ .join("\n")
97
+
98
+ resulting_json = "\n\tval properties = JSONObject().apply {\n" +
99
+ +split_properties.to_s
100
+ resulting_json += "\n\t}\n"
101
+ end
102
+
103
+ def special_property?(type)
104
+ !NATIVE_TYPES.include?(type.chomp('?'))
105
+ end
106
+
107
+ def optional_property?(type)
108
+ type.include?('?')
109
+ end
110
+
111
+ def titleize(str)
112
+ str.split('_').collect(&:capitalize).join
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,68 @@
1
+ module Evva
2
+ class Config
3
+ def initialize(hash:)
4
+ @hash = hash.deep_symbolize
5
+ @hash.validate_structure!(CONFIG_STRUCT)
6
+
7
+ unless dict_struct = DICTIONARY_STRUCT[@hash[:data_source][:type]]
8
+ raise ArgumentError, "unknown data source type '#{@hash[:data_source][:type]}'"
9
+ end
10
+
11
+ @hash[:data_source].validate_structure!(dict_struct)
12
+ end
13
+
14
+ def to_h
15
+ @hash
16
+ end
17
+
18
+ def data_source
19
+ @hash[:data_source]
20
+ end
21
+
22
+ def type
23
+ @hash[:type]
24
+ end
25
+
26
+ def out_path
27
+ @hash[:out_path]
28
+ end
29
+
30
+ def event_file_name
31
+ @hash[:event_file_name]
32
+ end
33
+
34
+ def people_file_name
35
+ @hash[:people_file_name]
36
+ end
37
+
38
+ def event_enum_file_name
39
+ @hash[:event_enum_file_name]
40
+ end
41
+
42
+ CONFIG_STRUCT = {
43
+ type: Hash,
44
+ elements: {
45
+ type: { type: String },
46
+ data_source: { type: Hash, elements: {
47
+ type: { type: String }
48
+ } },
49
+ out_path: { type: String },
50
+ event_file_name: { type: String },
51
+ event_enum_file_name: { type: String },
52
+ people_file_name: { type: String }
53
+ }
54
+ }.freeze
55
+
56
+ GOOGLE_SHEET_STRUCT = {
57
+ type: Hash,
58
+ elements: {
59
+ type: { type: String },
60
+ sheet_id: { type: String }
61
+ }
62
+ }.freeze
63
+
64
+ DICTIONARY_STRUCT = {
65
+ 'google_sheet' => GOOGLE_SHEET_STRUCT
66
+ }.freeze
67
+ end
68
+ end
@@ -0,0 +1,23 @@
1
+ module Evva
2
+ class DataSource
3
+ def initialize(keys)
4
+ unless keys.is_a?(Hash)
5
+ raise ArgumentError, "keys: expected Hash, got #{keys.class}"
6
+ end
7
+
8
+ keys.each do |property, v|
9
+ unless v.is_a?(Hash)
10
+ raise ArgumentError, "keys['#{property}']: expected Hash, got #{v.class}"
11
+ end
12
+
13
+ v.each do |key, v|
14
+ unless v.is_a?(String) || v.nil?
15
+ raise ArgumentError, "keys['#{property}']['#{key}']: expected String, got #{v.class}"
16
+ end
17
+ end
18
+ end
19
+
20
+ @keys = Hash[keys.map { |k, v| [k.downcase, v] }]
21
+ end
22
+ end
23
+ end