ios_analytics_cli 1.0.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 +135 -0
- data/.rubocop.yml +2 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +62 -0
- data/LICENSE.txt +21 -0
- data/README.md +48 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/analytics +34 -0
- data/ios_analytics_cli.gemspec +35 -0
- data/lib/ios_analytics_cli.rb +52 -0
- data/lib/ios_analytics_cli/commands/command.rb +6 -0
- data/lib/ios_analytics_cli/commands/generate.rb +77 -0
- data/lib/ios_analytics_cli/commands/init.rb +55 -0
- data/lib/ios_analytics_cli/error_handler.rb +22 -0
- data/lib/ios_analytics_cli/helpers/terminal.rb +11 -0
- data/lib/ios_analytics_cli/info.rb +16 -0
- data/lib/ios_analytics_cli/interactors/event.rb +22 -0
- data/lib/ios_analytics_cli/interactors/interactor.rb +8 -0
- data/lib/ios_analytics_cli/interactors/user_property.rb +12 -0
- data/lib/ios_analytics_cli/io/config.rb +18 -0
- data/lib/ios_analytics_cli/io/io.rb +10 -0
- data/lib/ios_analytics_cli/serializers/base.rb +43 -0
- data/lib/ios_analytics_cli/serializers/objc/analytics.rb +126 -0
- data/lib/ios_analytics_cli/serializers/objc/enums.rb +67 -0
- data/lib/ios_analytics_cli/serializers/objc/event.rb +182 -0
- data/lib/ios_analytics_cli/serializers/objc/logger.rb +53 -0
- data/lib/ios_analytics_cli/serializers/objc/objc.rb +23 -0
- data/lib/ios_analytics_cli/serializers/objc/screen.rb +90 -0
- data/lib/ios_analytics_cli/serializers/objc/user_property.rb +120 -0
- data/lib/ios_analytics_cli/serializers/swift/analytics.rb +63 -0
- data/lib/ios_analytics_cli/serializers/swift/event.rb +121 -0
- data/lib/ios_analytics_cli/serializers/swift/logger.rb +38 -0
- data/lib/ios_analytics_cli/serializers/swift/screen.rb +55 -0
- data/lib/ios_analytics_cli/serializers/swift/swift.rb +20 -0
- data/lib/ios_analytics_cli/serializers/swift/user_property.rb +69 -0
- data/lib/ios_analytics_cli/templates/header.erb +7 -0
- data/lib/ios_analytics_cli/templates/templates.rb +12 -0
- data/lib/ios_analytics_cli/version.rb +4 -0
- metadata +172 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'commander/blank'
|
2
|
+
require 'commander/command'
|
3
|
+
|
4
|
+
module Analytics
|
5
|
+
module Command
|
6
|
+
# A class that's called from CLI's 'generate' argument,
|
7
|
+
# and used to generate analytics files from the resource JSON file.
|
8
|
+
class Generate
|
9
|
+
include Helpers::Terminal
|
10
|
+
|
11
|
+
def perform
|
12
|
+
unless File.file?(config_src_path)
|
13
|
+
prompt.error("Unable to find #{config_src_path}")
|
14
|
+
return
|
15
|
+
end
|
16
|
+
prompt.say('Generating analytics files...')
|
17
|
+
generate
|
18
|
+
prompt.ok('Analytics files are successfully generated!')
|
19
|
+
end
|
20
|
+
|
21
|
+
## The following methods are used to generate analytics files,
|
22
|
+
## based on settings defined in 'analytics.yml' configuration file.
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
## Retrieving values from configuration file.
|
27
|
+
|
28
|
+
def config
|
29
|
+
Analytics::IO::Config.read
|
30
|
+
end
|
31
|
+
|
32
|
+
def config_lang
|
33
|
+
config[:language]
|
34
|
+
end
|
35
|
+
|
36
|
+
def config_analytics_path
|
37
|
+
config[:analyticsFilesPath]
|
38
|
+
end
|
39
|
+
|
40
|
+
def config_src_path
|
41
|
+
config[:sourcePath]
|
42
|
+
end
|
43
|
+
|
44
|
+
## Generating
|
45
|
+
|
46
|
+
def generate
|
47
|
+
json_file = File.open(config_src_path)
|
48
|
+
json_src = JSON.load(json_file)
|
49
|
+
|
50
|
+
case config_lang
|
51
|
+
when 'swift'
|
52
|
+
FileUtils.mkdir_p config_analytics_path unless File.exist? config_analytics_path
|
53
|
+
Analytics::Serializer::Swift.new(json_src).save(config_analytics_path)
|
54
|
+
when 'objc'
|
55
|
+
FileUtils.mkdir_p config_analytics_path unless File.exist? config_analytics_path
|
56
|
+
Analytics::Serializer::ObjC.new(json_src).save(config_analytics_path)
|
57
|
+
when 'swift-objc'
|
58
|
+
|
59
|
+
swift_path = config_analytics_path + '/Swift'
|
60
|
+
objc_path = config_analytics_path + '/ObjC'
|
61
|
+
|
62
|
+
FileUtils.mkdir_p swift_path unless File.exist? swift_path
|
63
|
+
FileUtils.mkdir_p objc_path unless File.exist? objc_path
|
64
|
+
|
65
|
+
Analytics::Serializer::Swift.new(json_src).save(swift_path)
|
66
|
+
Analytics::Serializer::ObjC.new(json_src).save(objc_path)
|
67
|
+
else
|
68
|
+
prompt.error("Unsupported value '#{config_lang}' for 'language'. Supported values are:\n"\
|
69
|
+
"- 'swift' - if You're using only Swift in Your project,\n"\
|
70
|
+
"- 'objc' - if You're using only ObjC in Your project,\n"\
|
71
|
+
"- 'swift-objc' if You're using both Swift & ObjC in Your project.")
|
72
|
+
return
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'commander/blank'
|
2
|
+
require 'commander/command'
|
3
|
+
|
4
|
+
module Analytics
|
5
|
+
module Command
|
6
|
+
# A class called from CLI's 'init' argument,
|
7
|
+
# which generates the project's configuration file depending on the user's input.
|
8
|
+
class Init
|
9
|
+
include Helpers::Terminal
|
10
|
+
|
11
|
+
# Entry point of this class - the only public method in it.
|
12
|
+
def perform
|
13
|
+
lang = lang_selection
|
14
|
+
analytics_path = analytics_path_selection
|
15
|
+
src_path = src_path_selection
|
16
|
+
|
17
|
+
config = {
|
18
|
+
language: lang,
|
19
|
+
analyticsFilesPath: analytics_path,
|
20
|
+
sourcePath: src_path
|
21
|
+
}
|
22
|
+
Analytics::IO::Config.write(config)
|
23
|
+
|
24
|
+
prompt.ok('Configuration file is properly created. You\'re good to go!')
|
25
|
+
end
|
26
|
+
|
27
|
+
## The following methods are used to retrieve project information,
|
28
|
+
## in order to create analytitcs.yml configuration file.
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Prompts a user to select a programming language.
|
33
|
+
def lang_selection
|
34
|
+
languages = {
|
35
|
+
'Swift' => 'swift',
|
36
|
+
'Objective-C' => 'objc',
|
37
|
+
'Both' => 'swift-objc'
|
38
|
+
}
|
39
|
+
prompt.select('Select lang:', languages)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Prompts a user to select a path to which the generated
|
43
|
+
# analytics files will be saved.
|
44
|
+
def analytics_path_selection
|
45
|
+
prompt.ask('Where would You like to store analytics files? (Press enter to use default path)', default: './Common/Analytics/')
|
46
|
+
end
|
47
|
+
|
48
|
+
# Prompts a user to select a path in which he'll hold a source json file,
|
49
|
+
# from which the analytics files will be generated.
|
50
|
+
def src_path_selection
|
51
|
+
prompt.ask('Where would You like to store json file from which analytics files will be generated? (Press enter to use default path)', default: 'analytics-source.json')
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Analytics
|
2
|
+
class ErrorHandler
|
3
|
+
class << self
|
4
|
+
def rescuable
|
5
|
+
yield
|
6
|
+
rescue StandardError => e
|
7
|
+
handle(e)
|
8
|
+
end
|
9
|
+
|
10
|
+
def handle(e)
|
11
|
+
prompt.error(
|
12
|
+
case e
|
13
|
+
when Errno::ENOENT
|
14
|
+
"We could not find a file that we need:\n\n#{e.message}"
|
15
|
+
else
|
16
|
+
"An error happened. This might help:\n\n#{e.message}"
|
17
|
+
end
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Analytics
|
2
|
+
def self.summary
|
3
|
+
<<-Summary
|
4
|
+
CLI tool that generates Analytics files, for iOS project, from a JSON file.
|
5
|
+
Summary
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.description
|
9
|
+
<<-Desc
|
10
|
+
This gem is develop in order to reduce the time needed to even start with the implementation of Analytics in iOS apps,
|
11
|
+
by generating files, classes, methods & everything else that\'s required to call a specific event, change a user property,
|
12
|
+
or even track an open screen. By calling just 2 commands, analytics init & analytics generate,
|
13
|
+
all of these are generated for You from the appropriately configured JSON file.
|
14
|
+
Desc
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'tty-prompt'
|
2
|
+
|
3
|
+
module Analytics
|
4
|
+
module Interactor
|
5
|
+
class Event
|
6
|
+
# Returns all properties, from all events, which uses an enum as the value.
|
7
|
+
def self.all_enum_properties(events)
|
8
|
+
events
|
9
|
+
.reject { |event| event['properties'].nil? }
|
10
|
+
.flat_map { |event| event['properties'] }
|
11
|
+
.reject { |property| property['values'].nil? }
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.enum_properties_from_event(event)
|
15
|
+
properties = event['properties']
|
16
|
+
return nil if properties.nil?
|
17
|
+
|
18
|
+
properties.reject { |property| property['values'].nil? }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'tty-prompt'
|
2
|
+
|
3
|
+
module Analytics
|
4
|
+
module Interactor
|
5
|
+
class UserProperty
|
6
|
+
# Returns all user properties which uses an enum as the value.
|
7
|
+
def self.all_enum_user_properties(user_properties)
|
8
|
+
user_properties.reject { |up| up['values'].nil? }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Analytics::IO
|
2
|
+
# Config operates on .yml configuration file
|
3
|
+
# inside project's root folder.
|
4
|
+
class Config
|
5
|
+
# A path to the .yml configuration file.
|
6
|
+
CONFIG_PATH = "#{PROJ_DIR}/analytics.yml".freeze
|
7
|
+
|
8
|
+
# Writes to a configuration file.
|
9
|
+
def self.write(data)
|
10
|
+
File.open(CONFIG_PATH, 'w') { |file| YAML.dump(data, file) }
|
11
|
+
end
|
12
|
+
|
13
|
+
# Reads the configuration file.
|
14
|
+
def self.read
|
15
|
+
YAML.load_file(CONFIG_PATH)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Analytics
|
2
|
+
# IO is a module that holds all IO stuff,
|
3
|
+
# such as reading/writing to a file or similar.
|
4
|
+
module IO
|
5
|
+
# The directory of the current project.
|
6
|
+
# Currently it's just a `Dir.pwd`, but we'll may
|
7
|
+
# receive a path in the CLI command in future.
|
8
|
+
PROJ_DIR = Dir.pwd.freeze
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Analytics
|
2
|
+
module Serializer
|
3
|
+
class Base
|
4
|
+
def initialize(src)
|
5
|
+
@src = src
|
6
|
+
end
|
7
|
+
|
8
|
+
# An abstract method used to save a file at provided path.
|
9
|
+
def save(_path)
|
10
|
+
raise NotImplementedError, 'Abstract Method'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# String extensions - will be moved somewhere else. Somewhere..
|
17
|
+
class String
|
18
|
+
# Converts a string to snake_case.
|
19
|
+
def snake_case
|
20
|
+
gsub(/::/, '/')
|
21
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
22
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
23
|
+
.tr('-', '_')
|
24
|
+
.downcase
|
25
|
+
end
|
26
|
+
|
27
|
+
# Converts a string to camelCase.
|
28
|
+
def camel_case
|
29
|
+
gsub(' ', '_')
|
30
|
+
.gsub('-', '_')
|
31
|
+
.snake_case
|
32
|
+
.split('_')
|
33
|
+
.inject([]) { |buffer, e| buffer.push(buffer.empty? ? e : e.capitalize) }
|
34
|
+
.join
|
35
|
+
end
|
36
|
+
|
37
|
+
# Converts a string to PascalCase.
|
38
|
+
def pascal_case
|
39
|
+
val = camel_case
|
40
|
+
val[0] = val[0].capitalize
|
41
|
+
val
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require_relative '../base'
|
2
|
+
|
3
|
+
module Analytics
|
4
|
+
module Serializer
|
5
|
+
class ObjC < Base
|
6
|
+
# It's named AnalyticsFile instead of Analytics,
|
7
|
+
# because it failed to have a class which used the same name as the parent module.
|
8
|
+
class AnalyticsFile
|
9
|
+
def initialize(src)
|
10
|
+
@src = src
|
11
|
+
end
|
12
|
+
|
13
|
+
def save(path)
|
14
|
+
@class_name = 'Analytics'
|
15
|
+
|
16
|
+
@file_name = @class_name + '.h'
|
17
|
+
output_path = File.join(path, @file_name)
|
18
|
+
File.write(output_path, render_h)
|
19
|
+
|
20
|
+
@file_name = @class_name + '.m'
|
21
|
+
output_path = File.join(path, @file_name)
|
22
|
+
File.write(output_path, render_m)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def render_h
|
28
|
+
ERB.new(template_h, nil, '-').result(binding)
|
29
|
+
end
|
30
|
+
|
31
|
+
def template_h
|
32
|
+
<<~TEMPLATE
|
33
|
+
<%= ERB.new(Analytics::Templates.tmpl_at('header.erb')).result(binding) %>
|
34
|
+
|
35
|
+
#import <Foundation/Foundation.h>
|
36
|
+
#import "AnalyticsLogger.h"
|
37
|
+
#import "AnalyticsEvent.h"
|
38
|
+
#import "AnalyticsScreen.h"
|
39
|
+
#import "AnalyticsUserProperty.h"
|
40
|
+
|
41
|
+
NS_ASSUME_NONNULL_BEGIN
|
42
|
+
|
43
|
+
__swift_unavailable("This class is only available in ObjC.")
|
44
|
+
@interface Analytics : NSObject <AnalyticsLogger>
|
45
|
+
|
46
|
+
- (instancetype)init NS_UNAVAILABLE;
|
47
|
+
+ (instancetype)new NS_UNAVAILABLE;
|
48
|
+
|
49
|
+
- (instancetype)initWithLoggers:(NSArray<id<AnalyticsLogger>> *)loggers;
|
50
|
+
|
51
|
+
+ (instancetype)analyticsWithLoggers:(NSArray<id<AnalyticsLogger>> *)loggers;
|
52
|
+
|
53
|
+
@end
|
54
|
+
|
55
|
+
NS_ASSUME_NONNULL_END
|
56
|
+
TEMPLATE
|
57
|
+
end
|
58
|
+
|
59
|
+
def render_m
|
60
|
+
ERB.new(template_m, nil, '-').result(binding)
|
61
|
+
end
|
62
|
+
|
63
|
+
def template_m
|
64
|
+
<<~TEMPLATE
|
65
|
+
<%= ERB.new(Analytics::Templates.tmpl_at('header.erb')).result(binding) %>
|
66
|
+
|
67
|
+
#import "<%= @class_name + '.h' %>"
|
68
|
+
|
69
|
+
@interface Analytics ()
|
70
|
+
|
71
|
+
@property (nonatomic, strong, readonly) NSArray<id<AnalyticsLogger>> *loggers;
|
72
|
+
|
73
|
+
@end
|
74
|
+
|
75
|
+
@implementation Analytics
|
76
|
+
|
77
|
+
- (instancetype)initWithLoggers:(NSArray<id<AnalyticsLogger>> *)loggers
|
78
|
+
{
|
79
|
+
if (self = [super init]) {
|
80
|
+
_loggers = loggers;
|
81
|
+
}
|
82
|
+
return self;
|
83
|
+
}
|
84
|
+
|
85
|
+
+ (instancetype)analyticsWithLoggers:(NSArray<id<AnalyticsLogger>> *)loggers
|
86
|
+
{
|
87
|
+
return [[self alloc] initWithLoggers:loggers];
|
88
|
+
}
|
89
|
+
|
90
|
+
#pragma mark - AnalyticsLogger
|
91
|
+
|
92
|
+
- (void)setEnabled:(BOOL)enabled
|
93
|
+
{
|
94
|
+
for (id<AnalyticsLogger> logger in self.loggers) {
|
95
|
+
[logger setEnabled:enabled];
|
96
|
+
}
|
97
|
+
}
|
98
|
+
|
99
|
+
- (void)logEvent:(AnalyticsEvent *)event
|
100
|
+
{
|
101
|
+
for (id<AnalyticsLogger> logger in self.loggers) {
|
102
|
+
[logger logEvent:event];
|
103
|
+
}
|
104
|
+
}
|
105
|
+
|
106
|
+
- (void)trackScreen:(AnalyticsScreen *)screen
|
107
|
+
{
|
108
|
+
for (id<AnalyticsLogger> logger in self.loggers) {
|
109
|
+
[logger trackScreen:screen];
|
110
|
+
}
|
111
|
+
}
|
112
|
+
|
113
|
+
- (void)setUserProperty:(AnalyticsUserProperty *)userProperty
|
114
|
+
{
|
115
|
+
for (id<AnalyticsLogger> logger in self.loggers) {
|
116
|
+
[logger setUserProperty:userProperty];
|
117
|
+
}
|
118
|
+
}
|
119
|
+
|
120
|
+
@end
|
121
|
+
TEMPLATE
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require_relative '../base'
|
2
|
+
|
3
|
+
module Analytics
|
4
|
+
module Serializer
|
5
|
+
class ObjC < Base
|
6
|
+
# It's named AnalyticsFile instead of Analytics,
|
7
|
+
# because it failed to have a class which used the same name as the parent module.
|
8
|
+
class Enums
|
9
|
+
def initialize(src)
|
10
|
+
@src = src
|
11
|
+
end
|
12
|
+
|
13
|
+
def save(path)
|
14
|
+
@class_name = 'AnalyticsEnums'
|
15
|
+
|
16
|
+
@file_name = @class_name + '.h'
|
17
|
+
output_path = File.join(path, @file_name)
|
18
|
+
File.write(output_path, render_h)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def render_h
|
24
|
+
ERB.new(template_h, nil, '-').result(binding)
|
25
|
+
end
|
26
|
+
|
27
|
+
def template_h
|
28
|
+
<<~TEMPLATE
|
29
|
+
<%= ERB.new(Analytics::Templates.tmpl_at('header.erb')).result(binding) %>
|
30
|
+
|
31
|
+
#import <Foundation/Foundation.h>
|
32
|
+
|
33
|
+
NS_ASSUME_NONNULL_BEGIN
|
34
|
+
|
35
|
+
#pragma mark - Analytics Event Enums (AE)
|
36
|
+
<% Interactor::Event.all_enum_properties(@src['events']).each do |property| -%>
|
37
|
+
<% @enum_name = 'AE' + property['name'].pascal_case -%>
|
38
|
+
|
39
|
+
__swift_unavailable("This enum is only available in ObjC.")
|
40
|
+
typedef NS_CLOSED_ENUM(NSInteger, <%= @enum_name %>) {
|
41
|
+
<% property['values'].each do |value| -%>
|
42
|
+
<%= @enum_name + value.pascal_case %>,
|
43
|
+
<% end -%>
|
44
|
+
};
|
45
|
+
<% end -%>
|
46
|
+
|
47
|
+
|
48
|
+
#pragma mark - Analytics User Property Enums (AUP)
|
49
|
+
<% Interactor::UserProperty.all_enum_user_properties(@src['userProperties']).each do |property| -%>
|
50
|
+
<% @enum_name = 'AUP' + property['name'].pascal_case -%>
|
51
|
+
|
52
|
+
__swift_unavailable("This enum is only available in ObjC.")
|
53
|
+
typedef NS_CLOSED_ENUM(NSInteger, <%= @enum_name %>) {
|
54
|
+
<% property['values'].each do |value| -%>
|
55
|
+
<%= @enum_name + value.pascal_case %>,
|
56
|
+
<% end -%>
|
57
|
+
<%= @enum_name + 'Nil' %>
|
58
|
+
};
|
59
|
+
<% end -%>
|
60
|
+
|
61
|
+
NS_ASSUME_NONNULL_END
|
62
|
+
TEMPLATE
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|