cocoapods-entitlements-statistics 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/Gemfile +18 -0
- data/LICENSE.txt +22 -0
- data/README.md +134 -0
- data/Rakefile +13 -0
- data/cocoapods-entitlements-statistics.gemspec +27 -0
- data/lib/cocoapods-entitlements-statistics/app_entitlements_statistics/analyze.rb +218 -0
- data/lib/cocoapods-entitlements-statistics/app_entitlements_statistics/core_ext/inflector.rb +35 -0
- data/lib/cocoapods-entitlements-statistics/app_entitlements_statistics/core_ext/try.rb +112 -0
- data/lib/cocoapods-entitlements-statistics/app_entitlements_statistics/entitlements.rb +185 -0
- data/lib/cocoapods-entitlements-statistics/app_entitlements_statistics/extracter.rb +112 -0
- data/lib/cocoapods-entitlements-statistics/app_entitlements_statistics/helper.rb +90 -0
- data/lib/cocoapods-entitlements-statistics/app_entitlements_statistics/info_plist.rb +202 -0
- data/lib/cocoapods-entitlements-statistics/app_entitlements_statistics/mobile_provision.rb +317 -0
- data/lib/cocoapods-entitlements-statistics/gem_version.rb +3 -0
- data/lib/cocoapods-entitlements-statistics/project_info.rb +82 -0
- data/lib/cocoapods-entitlements-statistics.rb +1 -0
- data/lib/cocoapods_plugin.rb +34 -0
- data/spec/command/statistics_spec.rb +12 -0
- data/spec/spec_helper.rb +50 -0
- metadata +161 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b18c67b431404139e32390800cee36de2c9443a053716f29832d32d22424954a
|
4
|
+
data.tar.gz: be771fb8171db38589b66f32155b946958d5d34b79b4ace92f8e8e6ea146bbd5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e3596496a3b67f9ae0cb268fe08118f415ca5e9073cea82017bb8176cef932158b44768cfbcebb9b7185b74ff293420895f2221c8aa02d069ebae6dc04305c70
|
7
|
+
data.tar.gz: 9c740e1c280ab955be62a9f51a7678bc07a50c5a28d16049cedf1964283f17ec0c12d87a6cab6710c0540821f6da7a888130264b3e7ca0d72b63a2288951f1a1
|
data/.gitignore
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in cocoapods-entitlements-statistics.gemspec
|
4
|
+
gemspec
|
5
|
+
gem 'crimp'
|
6
|
+
gem 'zip'
|
7
|
+
|
8
|
+
group :development do
|
9
|
+
gem 'cocoapods'
|
10
|
+
|
11
|
+
gem 'mocha'
|
12
|
+
gem 'bacon'
|
13
|
+
gem 'mocha-on-bacon'
|
14
|
+
gem 'prettybacon'
|
15
|
+
gem 'crimp'
|
16
|
+
gem 'zip'
|
17
|
+
|
18
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2023 bin <tang.bin@olaola.chat>
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
# cocoapods-entitlements-statistics
|
2
|
+
|
3
|
+
iOS app 权限统计, 使用cocoapods plugin 触发
|
4
|
+
|
5
|
+
|
6
|
+
## 安装
|
7
|
+
|
8
|
+
```shell
|
9
|
+
# 下载Releases v0.0.1 gem文件,然后进入下载目录执行下面命令
|
10
|
+
sudo gem install cocoapods-entitlements-statistics-0.0.1.gem
|
11
|
+
```
|
12
|
+
|
13
|
+
## 使用
|
14
|
+
|
15
|
+
在 Podfile 文件添加插件引用
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
#引入插件, 可以自定义 report_path, 默认在(iOS)项目根目录
|
19
|
+
plugin 'cocoapods-entitlements-statistics', :report_path => '/Users/rd01/Desktop'
|
20
|
+
#plugin 'cocoapods-entitlements-statistics'
|
21
|
+
|
22
|
+
target 'xxx' do
|
23
|
+
....
|
24
|
+
|
25
|
+
```
|
26
|
+
|
27
|
+
执行 pod install 会输出统计报告路径
|
28
|
+
|
29
|
+
```shell
|
30
|
+
$ pod install
|
31
|
+
.....
|
32
|
+
[!] Entitlements Statistics Report : path/to/entitlements_statistics/analyze_report
|
33
|
+
Pod installation complete! There are 75 dependencies from the Podfile and 116 total pods installed.
|
34
|
+
|
35
|
+
```
|
36
|
+
|
37
|
+
## 说明
|
38
|
+
|
39
|
+
##### 版本权限统计
|
40
|
+
|
41
|
+
默认会在 ~/appInfo-#{app_bundle_id}/ 下为每个版本建立权限统计文件
|
42
|
+
|
43
|
+
```yaml
|
44
|
+
|
45
|
+
├── appInfo-com.ola.chat
|
46
|
+
│ ├── entitlements_5.2.0.yml
|
47
|
+
│ ├── entitlements_5.3.0.yml
|
48
|
+
│ └── entitlements_versions.yml
|
49
|
+
```
|
50
|
+
|
51
|
+
##### 报告内容如下:
|
52
|
+
|
53
|
+
* 权限变化对比分析(增、删、改)
|
54
|
+
* (对比的各)版本权限列表详情
|
55
|
+
|
56
|
+
```yaml
|
57
|
+
|
58
|
+
compared 5.3.0 5.2.0
|
59
|
+
|
60
|
+
modify capabilitys :
|
61
|
+
5.3.0
|
62
|
+
- items ..
|
63
|
+
5.2.0
|
64
|
+
- items ..
|
65
|
+
------------------------------
|
66
|
+
add capabilitys :
|
67
|
+
- items ..
|
68
|
+
------------------------------
|
69
|
+
remove capabilitys :
|
70
|
+
- items ..
|
71
|
+
------------------------------
|
72
|
+
|
73
|
+
5.3.0 entitlements list:
|
74
|
+
...
|
75
|
+
|
76
|
+
5.2.0 entitlements list:
|
77
|
+
...
|
78
|
+
|
79
|
+
```
|
80
|
+
|
81
|
+
## iOS 项目权限
|
82
|
+
|
83
|
+
#### 大致分类 Capabilitys、info.plist(Cocoa Keys)
|
84
|
+
|
85
|
+
* Capabilitys
|
86
|
+
|
87
|
+
> Xcode->Target->Signing&Capabilitys->Capabilitys 下添加和删除 Capabilitys , 这里不是随便添加的,需要首先在苹果开发者后台注册或者声明对应Capabilitys. 更新的Provisioning Profile文件,其中包含Entitlements 字段包含(几乎所有的)已注册Capabilitys, Xcode中添加Capabilitys后会同Profile文件校验。不匹配则无法通过编译签名。
|
88
|
+
|
89
|
+
Supported capabilities (iOS):
|
90
|
+
https://developer.apple.com/help/account/reference/supported-capabilities-ios
|
91
|
+
|
92
|
+
* info.plist (Cocoa Key-Values)
|
93
|
+
* NS{Permissions}UsageDescription
|
94
|
+
> info.plist 中声明使用相机、麦克风、摄像头..等权限的说明
|
95
|
+
|
96
|
+
* 其它权限声明Keys
|
97
|
+
> info.plist 中也会包含一些权限的详细设置、声明,比如屏幕高刷、代理、后台模式...
|
98
|
+
|
99
|
+
info.plist 涉及的所有 Cocoa Keys
|
100
|
+
https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW1
|
101
|
+
|
102
|
+
#### 涉及的文件
|
103
|
+
|
104
|
+
* info.plist
|
105
|
+
* Runner.entitlements Capabilitys被添加后的,Xcode自动创建plist格式文件,里面包含相关Capability的声明、设置
|
106
|
+
* {embedded/xxx-xx-xx}.mobileprovision
|
107
|
+
包括里面的Entitlements字段, 保存一些 Capabilitys被添加后的 声明、设置
|
108
|
+
* Runner.xcodeproj
|
109
|
+
工程文件SystemCapabilities字段包含了一些Capabilitys声明
|
110
|
+
|
111
|
+
相关文档:
|
112
|
+
|
113
|
+
xx.mobileprovision
|
114
|
+
https://developer.apple.com/forums/thread/685723
|
115
|
+
|
116
|
+
Runner.entitlements
|
117
|
+
https://developer.apple.com/library/archive/documentation/Miscellaneous/Reference/EntitlementKeyReference/Chapters/AboutEntitlements.html#//apple_ref/doc/uid/TP40011195-CH1-SW1
|
118
|
+
|
119
|
+
#### 问题
|
120
|
+
|
121
|
+
* IPA包中找不到 In-App Purchase 功能标记
|
122
|
+
|
123
|
+
> 一般Capabilitys添加后会在Runner.entitlements、xx.mobileprovision找到对应key:value声明、设置
|
124
|
+
|
125
|
+
添加 In-App Purchase后, 仅在Runner.xcodeproj中有 "com.apple.InAppPurchase"=>{"enabled"=>"1"} 声明
|
126
|
+
|
127
|
+
* Background Modes 属于 Capabilitys 中的异类
|
128
|
+
|
129
|
+
> 一般Capabilitys添加后会在Runner.entitlements、xx.mobileprovision找到对应key:value声明、设置, 而Background Modes在这里没有任何记录
|
130
|
+
|
131
|
+
Background Modes的声明位置:
|
132
|
+
* Runner.xcodeproj : "com.apple.BackgroundModes"=>{"enabled"=>"1"}
|
133
|
+
* info.plist : "UIBackgroundModes"=>{"audio"、"remote-notification" ...}
|
134
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'cocoapods-entitlements-statistics/gem_version.rb'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'cocoapods-entitlements-statistics'
|
8
|
+
spec.version = CocoapodsEntitlementsStatistics::VERSION
|
9
|
+
spec.authors = ['bin']
|
10
|
+
spec.email = ['tang.bin@olaola.chat']
|
11
|
+
spec.description = %q{A short description of cocoapods-entitlements-statistics.}
|
12
|
+
spec.summary = %q{A longer description of cocoapods-entitlements-statistics.}
|
13
|
+
spec.homepage = 'https://github.com/EXAMPLE/cocoapods-entitlements-statistics'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_runtime_dependency "yaml"
|
22
|
+
spec.add_runtime_dependency "crimp"
|
23
|
+
spec.add_dependency 'CFPropertyList', '< 3.1.0', '>= 2.3.4'
|
24
|
+
spec.add_development_dependency 'bundler', '>= 1.12'
|
25
|
+
spec.add_dependency 'rubyzip', '>= 1.2', '< 3.0'
|
26
|
+
spec.add_development_dependency 'rake'
|
27
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "helper"
|
4
|
+
require 'fileutils'
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
module AppEntitlementsStatistics
|
8
|
+
|
9
|
+
class Analyzer
|
10
|
+
include Helper::Utils
|
11
|
+
|
12
|
+
attr_reader :store_path
|
13
|
+
attr_reader :report_path
|
14
|
+
attr_reader :identifier
|
15
|
+
|
16
|
+
def initialize(identifier,report_path: nil, store_path: nil)
|
17
|
+
@identifier = identifier
|
18
|
+
@report_path = report_path
|
19
|
+
@store_path = store_path
|
20
|
+
end
|
21
|
+
|
22
|
+
def analyze
|
23
|
+
load_version
|
24
|
+
return singleGenerate unless @versions.length > 1
|
25
|
+
pre_version = @versions[1]
|
26
|
+
cur_version = @versions[0]
|
27
|
+
pre_entitlements = load_entitlements_yaml(pre_version)
|
28
|
+
cur_entitlements = load_entitlements_yaml(cur_version)
|
29
|
+
return "" unless pre_entitlements.any?
|
30
|
+
return "" unless cur_entitlements.any?
|
31
|
+
|
32
|
+
capabilities_diff = analyze_capabilities_diff(pre_entitlements,cur_entitlements)
|
33
|
+
usage_descs_diff = analyze_usage_descs_diff(pre_entitlements,cur_entitlements);
|
34
|
+
report_path = generate(capabilities_diff,usage_descs_diff)
|
35
|
+
report_path
|
36
|
+
end
|
37
|
+
|
38
|
+
def singleGenerate
|
39
|
+
return "" unless @versions.length > 0
|
40
|
+
output = "no more versions to compare !! "
|
41
|
+
output += "\n\n\n#{@versions.first} entitlements list: "
|
42
|
+
output += "\n" + read_each_line(@versions.first)
|
43
|
+
output += "\n" + "----------------------------------------"
|
44
|
+
report_path = report_file_name(path: @report_path)
|
45
|
+
File.open(report_path, 'w') { |file|
|
46
|
+
file.write(output)
|
47
|
+
}
|
48
|
+
report_path
|
49
|
+
end
|
50
|
+
|
51
|
+
def generate(capabilities_diff,usage_descs_diff)
|
52
|
+
return unless @versions.length > 1
|
53
|
+
cur_version = @versions[0]
|
54
|
+
pre_version = @versions[1]
|
55
|
+
|
56
|
+
output = "\n" + "compared #{cur_version} #{pre_version}"
|
57
|
+
output += "\n" + capabilities_diff unless capabilities_diff.empty?
|
58
|
+
output += "\n" + usage_descs_diff unless usage_descs_diff.empty?
|
59
|
+
output += "\n\nThere is no difference between the two versions \n\n" unless (!capabilities_diff.empty? && !usage_descs_diff.empty?)
|
60
|
+
# puts output
|
61
|
+
|
62
|
+
output += "\n\n\n#{cur_version} entitlements list: "
|
63
|
+
output += "\n" + read_each_line(cur_version)
|
64
|
+
output += "\n" + "----------------------------------------"
|
65
|
+
output += "\n\n#{pre_version} entitlements list: "
|
66
|
+
output += "\n" + read_each_line(pre_version)
|
67
|
+
output += "\n" + "----------------------------------------"
|
68
|
+
|
69
|
+
report_path = report_file_name(path: @report_path)
|
70
|
+
File.open(report_path, 'w') { |file|
|
71
|
+
file.write(output)
|
72
|
+
}
|
73
|
+
report_path
|
74
|
+
end
|
75
|
+
|
76
|
+
def read_each_line(version)
|
77
|
+
file_name = entitlements_yaml_name(version,@identifier, path: @store_path)
|
78
|
+
file_content = ""
|
79
|
+
File.open(file_name,"r").each_line do |line|
|
80
|
+
file_content += "\n" + line
|
81
|
+
end
|
82
|
+
file_content
|
83
|
+
end
|
84
|
+
|
85
|
+
def load_version
|
86
|
+
yaml_name = versions_yaml_name(@identifier, path: @store_path)
|
87
|
+
if File.exist?(yaml_name)
|
88
|
+
@versions = YAML.load_file(yaml_name)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def load_entitlements_yaml(version)
|
93
|
+
yaml_name = entitlements_yaml_name(version,@identifier, path: @store_path)
|
94
|
+
yaml_content = [ ]
|
95
|
+
if File.exist?(yaml_name)
|
96
|
+
yaml_content = YAML.load_file(yaml_name)
|
97
|
+
end
|
98
|
+
puts "#{yaml_name} not found!!!" unless !yaml_content.empty?
|
99
|
+
yaml_content
|
100
|
+
end
|
101
|
+
|
102
|
+
def analyze_capabilities_diff(pre,cur)
|
103
|
+
output = ""
|
104
|
+
cur_capabilities_summary = cur['Capabilities_Summary']
|
105
|
+
pre_capabilities_summary = pre['Capabilities_Summary']
|
106
|
+
cur_capabilities = cur['Capabilities']
|
107
|
+
pre_capabilities = pre['Capabilities']
|
108
|
+
output = ""
|
109
|
+
# 修改
|
110
|
+
comm_capabilities = cur_capabilities_summary.keys & pre_capabilities_summary.keys
|
111
|
+
modifys = modifys_cur = modifys_pre = ""
|
112
|
+
comm_capabilities.each do |key|
|
113
|
+
if cur_capabilities_summary[key] != pre_capabilities_summary[key]
|
114
|
+
modifys_cur += "\n" + '- ' + key + ': ' + cur_capabilities[key].to_s
|
115
|
+
modifys_pre += "\n" + '- ' + key + ': ' + pre_capabilities[key].to_s
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
modifys += "\n" + @versions[0] unless modifys_cur.empty?
|
120
|
+
modifys += modifys_cur unless modifys_cur.empty?
|
121
|
+
modifys += "\n" + @versions[1] unless modifys_pre.empty?
|
122
|
+
modifys += modifys_pre unless modifys_pre.empty?
|
123
|
+
|
124
|
+
#新增
|
125
|
+
add_capabilities = cur_capabilities_summary.keys - pre_capabilities_summary.keys
|
126
|
+
adds = ""
|
127
|
+
add_capabilities.each do |key|
|
128
|
+
adds += "\n" + '- ' + key + ': ' + cur_capabilities[key].to_s
|
129
|
+
end
|
130
|
+
|
131
|
+
#移除
|
132
|
+
remove_capabilities = pre_capabilities_summary.keys - cur_capabilities_summary.keys
|
133
|
+
removes = ""
|
134
|
+
remove_capabilities.each do |key|
|
135
|
+
removes += "\n" + '- ' + key + ': ' + pre_capabilities[key].to_s
|
136
|
+
end
|
137
|
+
|
138
|
+
if !modifys.empty?
|
139
|
+
output += "\n" + "modify capabilitys : "
|
140
|
+
output += modifys
|
141
|
+
output += "\n------------------------------"
|
142
|
+
end
|
143
|
+
|
144
|
+
if !adds.empty?
|
145
|
+
output += "\n" + "add capabilitys : "
|
146
|
+
output += adds
|
147
|
+
output += "\n------------------------------"
|
148
|
+
end
|
149
|
+
|
150
|
+
if !removes.empty?
|
151
|
+
output += "\n" + "remove capabilitys : "
|
152
|
+
output += removes
|
153
|
+
output += "\n------------------------------"
|
154
|
+
end
|
155
|
+
|
156
|
+
output
|
157
|
+
end
|
158
|
+
|
159
|
+
def analyze_usage_descs_diff(pre,cur)
|
160
|
+
output = ""
|
161
|
+
cur_usage_descs_summary = cur['PermissionsUsageDescription_Summary']
|
162
|
+
pre_usage_descs_summary = pre['PermissionsUsageDescription_Summary']
|
163
|
+
|
164
|
+
cur_usage_descs = cur['PermissionsUsageDescription']
|
165
|
+
pre_usage_descs = pre['PermissionsUsageDescription']
|
166
|
+
|
167
|
+
# 修改
|
168
|
+
comm_usage_descs = cur_usage_descs_summary.keys & pre_usage_descs_summary.keys
|
169
|
+
modifys = modifys_cur = modifys_pre = ""
|
170
|
+
comm_usage_descs.each do |key|
|
171
|
+
if cur_usage_descs_summary[key] != pre_usage_descs_summary[key]
|
172
|
+
modifys_cur += "\n" + '- ' + key + ': ' + cur_usage_descs[key].to_s
|
173
|
+
modifys_pre += "\n" + '- ' + key + ': ' + pre_usage_descs[key].to_s
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
modifys += "\n" + @versions[0] unless modifys_cur.empty?
|
178
|
+
modifys += modifys_cur unless modifys_cur.empty?
|
179
|
+
modifys += "\n" + @versions[1] unless modifys_pre.empty?
|
180
|
+
modifys += modifys_pre unless modifys_pre.empty?
|
181
|
+
|
182
|
+
#新增
|
183
|
+
add_usage_descs = cur_usage_descs_summary.keys - pre_usage_descs_summary.keys
|
184
|
+
adds = ""
|
185
|
+
add_usage_descs.each do |key|
|
186
|
+
adds += "\n" + '- ' + key + ': ' + cur_usage_descs[key].to_s
|
187
|
+
end
|
188
|
+
|
189
|
+
#移除
|
190
|
+
remove_usage_descs = pre_usage_descs_summary.keys - cur_usage_descs_summary.keys
|
191
|
+
removes = ""
|
192
|
+
remove_usage_descs.each do |key|
|
193
|
+
removes += "\n" + '- ' + key + ': ' + pre_usage_descs[key].to_s
|
194
|
+
end
|
195
|
+
|
196
|
+
if !modifys.empty?
|
197
|
+
output += "\n" + "modify permissions usage description : "
|
198
|
+
output += modifys
|
199
|
+
output += "\n------------------------------"
|
200
|
+
end
|
201
|
+
|
202
|
+
if !adds.empty?
|
203
|
+
output += "\n" + "add permissions usage description : "
|
204
|
+
output += adds
|
205
|
+
output += "\n------------------------------"
|
206
|
+
end
|
207
|
+
|
208
|
+
if !removes.empty?
|
209
|
+
output += "\n" + "remove permissions usage description : "
|
210
|
+
output += removes
|
211
|
+
output += "\n------------------------------"
|
212
|
+
end
|
213
|
+
|
214
|
+
output
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppEntitlementsStatistics
|
4
|
+
module Inflector
|
5
|
+
def ai_snakecase
|
6
|
+
gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
7
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
8
|
+
.tr('-', '_')
|
9
|
+
.gsub(/\s/, '_')
|
10
|
+
.gsub(/__+/, '_')
|
11
|
+
.downcase
|
12
|
+
end
|
13
|
+
|
14
|
+
def ai_camelcase(first_letter: :upper, separators: ['-', '_', '\s'])
|
15
|
+
str = dup
|
16
|
+
|
17
|
+
separators.each do |s|
|
18
|
+
str = str.gsub(/(?:#{s}+)([a-z])/) { $1.upcase }
|
19
|
+
end
|
20
|
+
|
21
|
+
case first_letter
|
22
|
+
when :upper, true
|
23
|
+
str = str.gsub(/(\A|\s)([a-z])/) { $1 + $2.upcase }
|
24
|
+
when :lower, false
|
25
|
+
str = str.gsub(/(\A|\s)([A-Z])/) { $1 + $2.downcase }
|
26
|
+
end
|
27
|
+
|
28
|
+
str
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class String
|
34
|
+
include AppEntitlementsStatistics::Inflector
|
35
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyrights rails
|
4
|
+
# Copy from https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/object/try.rb
|
5
|
+
|
6
|
+
module AppEntitlementsStatistics
|
7
|
+
module Tryable # :nodoc:
|
8
|
+
##
|
9
|
+
# :method: try
|
10
|
+
#
|
11
|
+
# :call-seq:
|
12
|
+
# try(*a, &b)
|
13
|
+
#
|
14
|
+
# Invokes the public method whose name goes as first argument just like
|
15
|
+
# +public_send+ does, except that if the receiver does not respond to it the
|
16
|
+
# call returns +nil+ rather than raising an exception.
|
17
|
+
#
|
18
|
+
# This method is defined to be able to write
|
19
|
+
#
|
20
|
+
# @person.try(:name)
|
21
|
+
#
|
22
|
+
# instead of
|
23
|
+
#
|
24
|
+
# @person.name if @person
|
25
|
+
#
|
26
|
+
# +try+ calls can be chained:
|
27
|
+
#
|
28
|
+
# @person.try(:spouse).try(:name)
|
29
|
+
#
|
30
|
+
# instead of
|
31
|
+
#
|
32
|
+
# @person.spouse.name if @person && @person.spouse
|
33
|
+
#
|
34
|
+
# +try+ will also return +nil+ if the receiver does not respond to the method:
|
35
|
+
#
|
36
|
+
# @person.try(:non_existing_method) # => nil
|
37
|
+
#
|
38
|
+
# instead of
|
39
|
+
#
|
40
|
+
# @person.non_existing_method if @person.respond_to?(:non_existing_method) # => nil
|
41
|
+
#
|
42
|
+
# +try+ returns +nil+ when called on +nil+ regardless of whether it responds
|
43
|
+
# to the method:
|
44
|
+
#
|
45
|
+
# nil.try(:to_i) # => nil, rather than 0
|
46
|
+
#
|
47
|
+
# Arguments and blocks are forwarded to the method if invoked:
|
48
|
+
#
|
49
|
+
# @posts.try(:each_slice, 2) do |a, b|
|
50
|
+
# ...
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# The number of arguments in the signature must match. If the object responds
|
54
|
+
# to the method the call is attempted and +ArgumentError+ is still raised
|
55
|
+
# in case of argument mismatch.
|
56
|
+
#
|
57
|
+
# If +try+ is called without arguments it yields the receiver to a given
|
58
|
+
# block unless it is +nil+:
|
59
|
+
#
|
60
|
+
# @person.try do |p|
|
61
|
+
# ...
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# You can also call try with a block without accepting an argument, and the block
|
65
|
+
# will be instance_eval'ed instead:
|
66
|
+
#
|
67
|
+
# @person.try { upcase.truncate(50) }
|
68
|
+
#
|
69
|
+
# Please also note that +try+ is defined on +Object+. Therefore, it won't work
|
70
|
+
# with instances of classes that do not have +Object+ among their ancestors,
|
71
|
+
# like direct subclasses of +BasicObject+.
|
72
|
+
def try(method_name = nil, *args, &block)
|
73
|
+
if method_name.nil? && block_given?
|
74
|
+
if block.arity.zero?
|
75
|
+
instance_eval(&b)
|
76
|
+
else
|
77
|
+
yield self
|
78
|
+
end
|
79
|
+
elsif respond_to?(method_name)
|
80
|
+
public_send(method_name, *args, &block)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# :method: try!
|
86
|
+
#
|
87
|
+
# :call-seq:
|
88
|
+
# try!(*a, &b)
|
89
|
+
#
|
90
|
+
# Same as #try, but raises a +NoMethodError+ exception if the receiver is
|
91
|
+
# not +nil+ and does not implement the tried method.
|
92
|
+
#
|
93
|
+
# "a".try!(:upcase) # => "A"
|
94
|
+
# nil.try!(:upcase) # => nil
|
95
|
+
# 123.try!(:upcase) # => NoMethodError: undefined method `upcase' for 123:Integer
|
96
|
+
def try!(method_name = nil, *args, &block)
|
97
|
+
if method_name.nil? && block_given?
|
98
|
+
if block.arity.zero?
|
99
|
+
instance_eval(&block)
|
100
|
+
else
|
101
|
+
yield self
|
102
|
+
end
|
103
|
+
else
|
104
|
+
public_send(method_name, *args, &block)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
class Object
|
111
|
+
include AppEntitlementsStatistics::Tryable
|
112
|
+
end
|