fastlane 2.40.0.beta.20170622010014 → 2.40.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/cert/README.md +3 -1
  3. data/deliver/README.md +5 -1
  4. data/deliver/lib/deliver/options.rb +16 -0
  5. data/deliver/lib/deliver/runner.rb +36 -2
  6. data/fastlane/README.md +3 -1
  7. data/fastlane/lib/fastlane/actions/precheck.rb +52 -0
  8. data/fastlane/lib/fastlane/tools.rb +2 -1
  9. data/fastlane/lib/fastlane/version.rb +1 -1
  10. data/fastlane_core/README.md +2 -1
  11. data/frameit/README.md +3 -1
  12. data/gym/README.md +3 -1
  13. data/match/README.md +3 -1
  14. data/pem/README.md +3 -1
  15. data/pilot/README.md +3 -1
  16. data/precheck/README.md +174 -0
  17. data/precheck/lib/assets/PrecheckfileTemplate +28 -0
  18. data/precheck/lib/precheck.rb +22 -0
  19. data/precheck/lib/precheck/commands_generator.rb +56 -0
  20. data/precheck/lib/precheck/item_to_check.rb +58 -0
  21. data/precheck/lib/precheck/options.rb +63 -0
  22. data/precheck/lib/precheck/rule.rb +169 -0
  23. data/precheck/lib/precheck/rule_check_result.rb +19 -0
  24. data/precheck/lib/precheck/rule_processor.rb +199 -0
  25. data/precheck/lib/precheck/rules/abstract_text_match_rule.rb +64 -0
  26. data/precheck/lib/precheck/rules/copyright_date_rule.rb +38 -0
  27. data/precheck/lib/precheck/rules/curse_words_rule.rb +57 -0
  28. data/precheck/lib/precheck/rules/custom_text_rule.rb +36 -0
  29. data/precheck/lib/precheck/rules/future_functionality_rule.rb +34 -0
  30. data/precheck/lib/precheck/rules/negative_apple_sentiment_rule.rb +38 -0
  31. data/precheck/lib/precheck/rules/other_platforms_rule.rb +38 -0
  32. data/precheck/lib/precheck/rules/placeholder_words_rule.rb +33 -0
  33. data/precheck/lib/precheck/rules/rules_data/curse_word_hashes/en_us.txt +349 -0
  34. data/precheck/lib/precheck/rules/test_words_rule.rb +31 -0
  35. data/precheck/lib/precheck/rules/unreachable_urls_rule.rb +42 -0
  36. data/precheck/lib/precheck/runner.rb +164 -0
  37. data/produce/README.md +3 -1
  38. data/scan/README.md +3 -1
  39. data/screengrab/README.md +2 -1
  40. data/sigh/README.md +3 -1
  41. data/snapshot/README.md +3 -1
  42. data/spaceship/README.md +2 -1
  43. data/spaceship/lib/spaceship/tunes/language_item.rb +5 -1
  44. data/supply/README.md +3 -1
  45. metadata +38 -14
@@ -0,0 +1,28 @@
1
+ # For more information about this configuration visit
2
+ # https://github.com/fastlane/fastlane/tree/master/precheck#precheckfile
3
+
4
+ # In general, you can use the options available
5
+ # fastlane precheck --help
6
+
7
+ # Remove the # in front of the line to enable the option
8
+
9
+ # You have three possible values for each rule options
10
+ # :skip
11
+ # indicates that your metadata will not be checked by this rule
12
+
13
+ # :warn
14
+ # when triggered, this rule will warn you of a potential problem
15
+
16
+ # :fail
17
+ # when triggered, this rule will cause an error to be displayed and it will prevent any further fastlane commands from running after precheck finishes
18
+
19
+ # Examples:
20
+ # negative_apple_sentiment(level: :skip)
21
+ # curse_words(level: :warn)
22
+ # future_functionality(level: :error)
23
+ # other_platforms(level: :error)
24
+ # placeholder_text(level: :error)
25
+ # test_words(level: :error)
26
+ # unreachable_urls(level: :error)
27
+ # custom_text(data: ["fabric"], level: :warn)
28
+
@@ -0,0 +1,22 @@
1
+ require 'fastlane_core'
2
+ require 'precheck/runner'
3
+ require 'precheck/options'
4
+
5
+ module Precheck
6
+ # Use this to just setup the configuration attribute and set it later somewhere else
7
+ class << self
8
+ attr_accessor :config
9
+
10
+ def precheckfile_name
11
+ "Precheckfile"
12
+ end
13
+ end
14
+
15
+ Helper = FastlaneCore::Helper # you gotta love Ruby: Helper.* should use the Helper class contained in FastlaneCore
16
+ UI = FastlaneCore::UI
17
+ ROOT = Pathname.new(File.expand_path('../..', __FILE__))
18
+
19
+ ENV['APP_IDENTIFIER'] ||= ENV["PRECHECK_APP_IDENTIFIER"]
20
+
21
+ DESCRIPTION = 'Check your app using a community driven set of App Store review rules to avoid being rejected'
22
+ end
@@ -0,0 +1,56 @@
1
+ require "commander"
2
+ require "fastlane_core"
3
+ require "fastlane/version"
4
+
5
+ HighLine.track_eof = false
6
+
7
+ module Precheck
8
+ class CommandsGenerator
9
+ include Commander::Methods
10
+
11
+ def self.start
12
+ new.run
13
+ end
14
+
15
+ def run
16
+ program :name, 'precheck'
17
+ program :version, Fastlane::VERSION
18
+ program :description, Precheck::DESCRIPTION
19
+ program :help, "Author", "Joshua Liebowitz <taquitos@gmail.com>, @taquitos"
20
+ program :help, "Website", "https://fastlane.tools"
21
+ program :help, "GitHub", "https://github.com/fastlane/fastlane/tree/master/precheck"
22
+ program :help_formatter, :compact
23
+
24
+ global_option("--verbose") { FastlaneCore::Globals.verbose = true }
25
+
26
+ command :check_metadata do |c|
27
+ c.syntax = "fastlane precheck"
28
+ c.description = Precheck::DESCRIPTION
29
+
30
+ FastlaneCore::CommanderGenerator.new.generate(Precheck::Options.available_options, command: c)
31
+
32
+ c.action do |_args, options|
33
+ Precheck.config = FastlaneCore::Configuration.create(Precheck::Options.available_options, options.__hash__)
34
+ Precheck::Runner.new.run
35
+ end
36
+ end
37
+
38
+ command :init do |c|
39
+ c.syntax = "fastlane precheck init"
40
+ c.description = "Creates a new Precheckfile for you"
41
+ c.action do |_args, options|
42
+ containing = FastlaneCore::Helper.fastlane_enabled_folder_path
43
+ path = File.join(containing, Precheck.precheckfile_name)
44
+ UI.user_error! "Precheckfile already exists" if File.exist?(path)
45
+ template = File.read("#{Precheck::ROOT}/lib/assets/PrecheckfileTemplate")
46
+ File.write(path, template)
47
+ UI.success "Successfully created '#{path}'. Open the file using a code editor."
48
+ end
49
+ end
50
+
51
+ default_command :check_metadata
52
+
53
+ run!
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,58 @@
1
+ module Precheck
2
+ # each attribute on a app version is a single item.
3
+ # for example: .name, .keywords, .description, will all have a single item to represent them
4
+ # which includes their name and a more user-friendly name we can use to print out information
5
+ class ItemToCheck
6
+ attr_accessor :item_name
7
+ attr_accessor :friendly_name
8
+ attr_accessor :is_optional
9
+
10
+ def initialize(item_name, friendly_name, is_optional = false)
11
+ @item_name = item_name
12
+ @friendly_name = friendly_name
13
+ @is_optional = is_optional
14
+ end
15
+
16
+ def item_data
17
+ not_implemented(__method__)
18
+ end
19
+
20
+ def inspect
21
+ "#{self.class}(friendly_name: #{@friendly_name}, data: #{@item_data})"
22
+ end
23
+
24
+ def to_s
25
+ "#{self.class}: #{item_name}: #{friendly_name}"
26
+ end
27
+ end
28
+
29
+ # if the data point we want to check is a text field (like 'description'), we'll use this object to encapsulate it
30
+ # this includes the text, the property name, and what that name maps to in plain english so that we can print out nice, friendly messages.
31
+ class TextItemToCheck < ItemToCheck
32
+ attr_accessor :text
33
+
34
+ def initialize(text, item_name, friendly_name, is_optional = false)
35
+ @text = text
36
+ super(item_name, friendly_name, is_optional)
37
+ end
38
+
39
+ def item_data
40
+ return text
41
+ end
42
+ end
43
+
44
+ # if the data point we want to check is a URL field (like 'marketing_url'), we'll use this object to encapsulate it
45
+ # this includes the url, the property name, and what that name maps to in plain english so that we can print out nice, friendly messages.
46
+ class URLItemToCheck < ItemToCheck
47
+ attr_accessor :url
48
+
49
+ def initialize(url, item_name, friendly_name, is_optional = false)
50
+ @url = url
51
+ super(item_name, friendly_name, is_optional)
52
+ end
53
+
54
+ def item_data
55
+ return url
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,63 @@
1
+ require 'fastlane_core'
2
+ require 'credentials_manager'
3
+ Dir[File.dirname(__FILE__) + '/rules/*.rb'].each { |file| require file }
4
+
5
+ module Precheck
6
+ class Options
7
+ def self.rules
8
+ [
9
+ NegativeAppleSentimentRule,
10
+ PlaceholderWordsRule,
11
+ OtherPlatformsRule,
12
+ FutureFunctionalityRule,
13
+ TestWordsRule,
14
+ CurseWordsRule,
15
+ CustomTextRule,
16
+ CopyrightDateRule,
17
+ UnreachableURLRule
18
+ ].map(&:new)
19
+ end
20
+
21
+ def self.available_options
22
+ user = CredentialsManager::AppfileConfig.try_fetch_value(:itunes_connect_id)
23
+ user ||= CredentialsManager::AppfileConfig.try_fetch_value(:apple_id)
24
+
25
+ [
26
+ FastlaneCore::ConfigItem.new(key: :app_identifier,
27
+ short_option: "-a",
28
+ env_name: "PRECHECK_APP_IDENTIFIER",
29
+ description: "The bundle identifier of your app",
30
+ default_value: CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier)),
31
+ FastlaneCore::ConfigItem.new(key: :username,
32
+ short_option: "-u",
33
+ env_name: "PRECHECK_USERNAME",
34
+ description: "Your Apple ID Username",
35
+ default_value: user),
36
+ FastlaneCore::ConfigItem.new(key: :team_id,
37
+ short_option: "-b",
38
+ env_name: "PRECHECK_TEAM_ID",
39
+ description: "The ID of your iTunes Connect team if you're in multiple teams",
40
+ optional: true,
41
+ default_value: CredentialsManager::AppfileConfig.try_fetch_value(:team_id),
42
+ verify_block: proc do |value|
43
+ ENV["FASTLANE_ITC_TEAM_ID"] = value.to_s
44
+ end),
45
+ FastlaneCore::ConfigItem.new(key: :team_name,
46
+ short_option: "-l",
47
+ env_name: "PRECHECK_TEAM_NAME",
48
+ description: "The name of your iTunes Connect team if you're in multiple teams",
49
+ optional: true,
50
+ default_value: CredentialsManager::AppfileConfig.try_fetch_value(:team_name),
51
+ verify_block: proc do |value|
52
+ ENV["FASTLANE_ITC_TEAM_NAME"] = value.to_s
53
+ end),
54
+ FastlaneCore::ConfigItem.new(key: :default_rule_level,
55
+ short_option: "-r",
56
+ env_name: "PRECHECK_DEFAULT_RULE_LEVEL",
57
+ description: "The default rule level unless otherwise configured",
58
+ is_string: false,
59
+ default_value: RULE_LEVELS[:error])
60
+ ] + rules
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,169 @@
1
+ require 'fastlane_core'
2
+ require 'precheck/item_to_check'
3
+ require 'precheck/rule_check_result'
4
+
5
+ module Precheck
6
+ VALIDATION_STATES = {
7
+ passed: "passed",
8
+ failed: "failed",
9
+ skipped: "skipped"
10
+ }
11
+
12
+ # rules can cause warnings, errors, or be skipped all together
13
+ # by default they are set to indicate a RULE_LEVELS[:error]
14
+ RULE_LEVELS = {
15
+ warn: :warn,
16
+ error: :error,
17
+ skip: :skip
18
+ }
19
+
20
+ # Abstract super class
21
+ attr_accessor :rule_block
22
+
23
+ class Rule < FastlaneCore::ConfigItem
24
+ # when a rule evaluates a single item, it has a validation state
25
+ # if it fails, it has some data like what text failed to pass, or maybe a bad url
26
+ # this class encapsulates that return value and state
27
+ # it is the return value from each evaluated @rule_block
28
+ class RuleReturn
29
+ attr_accessor :validation_state
30
+ attr_accessor :failure_data
31
+
32
+ def initialize(validation_state: nil, failure_data: nil)
33
+ @validation_state = validation_state
34
+ @failure_data = failure_data
35
+ end
36
+ end
37
+
38
+ def initialize(short_option: nil,
39
+ verify_block: nil,
40
+ is_string: nil,
41
+ type: nil,
42
+ conflicting_options: nil,
43
+ conflict_block: nil,
44
+ deprecated: nil,
45
+ sensitive: nil,
46
+ display_in_shell: nil)
47
+
48
+ super(key: self.class.key,
49
+ env_name: self.class.env_name,
50
+ description: self.class.description,
51
+ short_option: short_option,
52
+ default_value: self.class.default_value,
53
+ verify_block: verify_block,
54
+ is_string: is_string,
55
+ type: type,
56
+ optional: true,
57
+ conflicting_options: conflicting_options,
58
+ conflict_block: conflict_block,
59
+ deprecated: deprecated,
60
+ sensitive: sensitive,
61
+ display_in_shell: display_in_shell)
62
+ end
63
+
64
+ def to_s
65
+ @key
66
+ end
67
+
68
+ def self.env_name
69
+ not_implemented(__method__)
70
+ end
71
+
72
+ def self.key
73
+ not_implemented(__method__)
74
+ end
75
+
76
+ def self.description
77
+ not_implemented(__method__)
78
+ end
79
+
80
+ def self.friendly_name
81
+ not_implemented(__method__)
82
+ end
83
+
84
+ def self.default_value
85
+ CredentialsManager::AppfileConfig.try_fetch_value(self.key)
86
+ end
87
+
88
+ def friendly_name
89
+ return self.class.friendly_name
90
+ end
91
+
92
+ def inspect
93
+ "#{self.class}(description: #{@description}, key: #{@key})"
94
+ end
95
+
96
+ # some rules can be customized with extra data at runtime, see CustomTextRule as an example
97
+ def needs_customization?
98
+ return false
99
+ end
100
+
101
+ # some rules can be customized with extra data at runtime, see CustomTextRule as an example
102
+ def customize_with_data(data: nil)
103
+ not_implemented(__method__)
104
+ end
105
+
106
+ # some rules only support specific fields, by default, all fields are supported unless restricted by
107
+ # providing a list of symbols matching the item_name as defined as the ItemToCheck is generated
108
+ def supported_fields_symbol_set
109
+ return nil
110
+ end
111
+
112
+ def rule_block
113
+ not_implemented(__method__)
114
+ end
115
+
116
+ def check_item(item)
117
+ # validate the item we have was properly matched to this rule: TextItem -> TextRule, URLItem -> URLRule
118
+ return skip_item_not_meant_for_this_rule(item) unless handle_item?(item)
119
+ return skip_item_not_meant_for_this_rule(item) unless item_field_supported?(item_name: item.item_name)
120
+
121
+ # do the actual checking now
122
+ return perform_check(item: item)
123
+ end
124
+
125
+ def skip_item_not_meant_for_this_rule(item)
126
+ # item isn't mean for this rule, which is fine, we can just keep passing it along
127
+ return nil
128
+ end
129
+
130
+ # each rule can define what type of ItemToCheck subclass they support
131
+ # override this method and return true or false
132
+ def handle_item?(item)
133
+ not_implemented(__method__)
134
+ end
135
+
136
+ def item_field_supported?(item_name: nil)
137
+ return true if supported_fields_symbol_set.nil?
138
+ return true if supported_fields_symbol_set.include?(item_name)
139
+ return false
140
+ end
141
+
142
+ def perform_check(item: nil)
143
+ if item.item_data.to_s == "" && item.is_optional
144
+ # item is optional, and empty, so that's totally fine
145
+ check_result = RuleReturn.new(validation_state: Precheck::VALIDATION_STATES[:passed])
146
+ return RuleCheckResult.new(item, check_result, self)
147
+ end
148
+
149
+ check_result = self.rule_block.call(item.item_data)
150
+ return RuleCheckResult.new(item, check_result, self)
151
+ end
152
+ end
153
+
154
+ # Rule types are more or less just marker classes that are intended to communicate what types of things each rule
155
+ # expects to deal with. TextRules deal with checking text values, URLRules will check url specific things like connectivity
156
+ # TextRules expect that text values will be passed to the rule_block, likewise, URLs are expected to be passed to the
157
+ # URLRule rule_block
158
+ class TextRule < Rule
159
+ def handle_item?(item)
160
+ (item.kind_of? TextItemToCheck) ? true : false
161
+ end
162
+ end
163
+
164
+ class URLRule < Rule
165
+ def handle_item?(item)
166
+ (item.kind_of? URLItemToCheck) ? true : false
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,19 @@
1
+ module Precheck
2
+ # after each item is checked for rule conformance to a single rule, a result is generated
3
+ # a result can have a status of success or fail for a given rule/item
4
+ class RuleCheckResult
5
+ attr_accessor :item
6
+ attr_accessor :rule_return # RuleReturn
7
+ attr_accessor :rule
8
+
9
+ def initialize(item, rule_return, rule)
10
+ @item = item
11
+ @rule_return = rule_return
12
+ @rule = rule
13
+ end
14
+
15
+ def status
16
+ rule_return.validation_state
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,199 @@
1
+ require 'spaceship'
2
+ require 'fastlane/markdown_table_formatter'
3
+ require 'precheck/item_to_check'
4
+
5
+ module Precheck
6
+ # encapsulated the results of the rule processing, needed to return not just an array of the results of our
7
+ # checks, but also an array of items we didn't check, just in-case we were expecting to check everything
8
+ class RuleProcessResult
9
+ attr_accessor :error_results # { rule: [result, result, ...] }
10
+ attr_accessor :warning_results # { rule: [result, result, ...] }
11
+ attr_accessor :skipped_rules
12
+ attr_accessor :items_not_checked
13
+
14
+ def initialize(error_results: nil,
15
+ warning_results: nil,
16
+ skipped_rules: nil,
17
+ items_not_checked: nil)
18
+ @error_results = error_results
19
+ @warning_results = warning_results
20
+ @skipped_rules = skipped_rules
21
+ @items_not_checked = items_not_checked
22
+ end
23
+
24
+ def should_trigger_user_error?
25
+ return true if error_results.length > 0
26
+ return false
27
+ end
28
+
29
+ def has_errors_or_warnings?
30
+ return true if error_results.length > 0 || warning_results.length > 0
31
+ end
32
+
33
+ def items_not_checked?
34
+ return true if items_not_checked.length > 0
35
+ end
36
+ end
37
+
38
+ class RuleProcessor
39
+ def self.process_app_and_version(app: nil, app_version: nil, rules: nil)
40
+ items_to_check = []
41
+ items_to_check += generate_text_items_to_check(app: app, app_version: app_version)
42
+ items_to_check += generate_url_items_to_check(app: app, app_version: app_version)
43
+
44
+ return process_rules(items_to_check: items_to_check, rules: rules)
45
+ end
46
+
47
+ def self.process_rules(items_to_check: nil, rules: nil)
48
+ items_not_checked = items_to_check.to_set # items we haven't checked by at least one rule
49
+ error_results = {} # rule to fields map
50
+ warning_results = {} # rule to fields map
51
+ skipped_rules = []
52
+
53
+ rules.each do |rule|
54
+ rule_config = Precheck.config[rule.key]
55
+ rule_level = rule_config[:level].to_sym unless rule_config.nil?
56
+ rule_level ||= Precheck.config[:default_rule_level]
57
+
58
+ if rule_level == RULE_LEVELS[:skip]
59
+ skipped_rules << rule
60
+ UI.message "Skipped: #{rule.class.friendly_name}-> #{rule.description}".yellow
61
+ next
62
+ end
63
+
64
+ if rule.needs_customization?
65
+ if rule_config.nil? || rule_config[:data].nil?
66
+ UI.verbose("#{rule.key} excluded because no data was passed to it e.g.: #{rule.key}(data: <data here>)")
67
+ next
68
+ end
69
+
70
+ custom_data = rule_config[:data]
71
+ rule.customize_with_data(data: custom_data)
72
+ end
73
+
74
+ # if the rule failed at least once, we won't print a success message
75
+ rule_failed_at_least_once = false
76
+
77
+ items_to_check.each do |item|
78
+ result = rule.check_item(item)
79
+
80
+ # each rule will determine if it can handle this item, if not, it will just pass nil back
81
+ next if result.nil?
82
+
83
+ # we've checked this item, remove it from list of items not checked
84
+ items_not_checked.delete(item)
85
+
86
+ # if we passed, then go to the next item, otherwise, recode the failure
87
+ next unless result.status == VALIDATION_STATES[:failed]
88
+ add_new_result_to_rule_hash(rule_hash: error_results, result: result) if rule_level == RULE_LEVELS[:error]
89
+ add_new_result_to_rule_hash(rule_hash: warning_results, result: result) if rule_level == RULE_LEVELS[:warn]
90
+ rule_failed_at_least_once = true
91
+ end
92
+
93
+ if rule_failed_at_least_once
94
+ UI.error "😵 Failed: #{rule.class.friendly_name}-> #{rule.description}"
95
+ else
96
+ UI.message "✅ Passed: #{rule.class.friendly_name}"
97
+ end
98
+ end
99
+
100
+ return RuleProcessResult.new(
101
+ error_results: error_results,
102
+ warning_results: warning_results,
103
+ skipped_rules: skipped_rules,
104
+ items_not_checked: items_not_checked.to_a
105
+ )
106
+ end
107
+
108
+ # hash will be { rule: [result, result, result] }
109
+ def self.add_new_result_to_rule_hash(rule_hash: nil, result: nil)
110
+ result_array = rule_hash[result.rule] ||= []
111
+ result_array << result
112
+ rule_hash[result.rule] = result_array
113
+ end
114
+
115
+ def self.generate_url_items_to_check(app: nil, app_version: nil)
116
+ items = []
117
+ items += collect_urls_from_hash(hash: app_version.support_url,
118
+ item_name: :support_url,
119
+ friendly_name_postfix: "support URL")
120
+ items += collect_urls_from_hash(hash: app_version.marketing_url,
121
+ item_name: :marketing_url,
122
+ friendly_name_postfix: "marketing URL",
123
+ is_optional: true)
124
+
125
+ items += collect_urls_from_hash(hash: app.details.privacy_url,
126
+ item_name: :privacy_url,
127
+ friendly_name_postfix: "privacy URL",
128
+ is_optional: true)
129
+ return items
130
+ end
131
+
132
+ def self.collect_urls_from_hash(hash: nil, item_name: nil, friendly_name_postfix: nil, is_optional: false)
133
+ items = []
134
+ hash.each do |key, value|
135
+ items << URLItemToCheck.new(value, item_name, "#{friendly_name_postfix}: (#{key})", is_optional)
136
+ end
137
+ return items
138
+ end
139
+
140
+ def self.generate_text_items_to_check(app: nil, app_version: nil)
141
+ items = []
142
+ items << TextItemToCheck.new(app_version.copyright, :copyright, "copyright")
143
+ items << TextItemToCheck.new(app_version.review_first_name, :review_first_name, "review first name")
144
+ items << TextItemToCheck.new(app_version.review_last_name, :review_last_name, "review last name")
145
+ items << TextItemToCheck.new(app_version.review_phone_number, :review_phone_number, "review phone number")
146
+ items << TextItemToCheck.new(app_version.review_email, :review_email, "review email")
147
+ items << TextItemToCheck.new(app_version.review_demo_user, :review_demo_user, "review demo user")
148
+ items << TextItemToCheck.new(app_version.review_notes, :review_notes, "review notes")
149
+
150
+ items += collect_text_items_from_language_item(hash: app_version.keywords,
151
+ item_name: :keywords,
152
+ friendly_name_postfix: "keywords")
153
+
154
+ items += collect_text_items_from_language_item(hash: app_version.description,
155
+ item_name: :description,
156
+ friendly_name_postfix: "description")
157
+
158
+ items += collect_text_items_from_language_item(hash: app_version.release_notes,
159
+ item_name: :release_notes,
160
+ friendly_name_postfix: "release notes")
161
+
162
+ items += collect_text_items_from_language_item(hash: app.details.name,
163
+ item_name: :app_name,
164
+ friendly_name_postfix: "app name")
165
+
166
+ items += collect_text_items_from_language_item(hash: app.details.apple_tv_privacy_policy,
167
+ item_name: :app_subtitle,
168
+ friendly_name_postfix: " tv privacy policy")
169
+
170
+ items += collect_text_items_from_language_item(hash: app.details.subtitle,
171
+ item_name: :app_subtitle,
172
+ friendly_name_postfix: "app name subtitle",
173
+ is_optional: true)
174
+ return items
175
+ end
176
+
177
+ # # a few attributes are LanguageItem this method creates a TextItemToCheck for each pair
178
+ def self.collect_text_items_from_language_item(hash: nil, item_name: nil, friendly_name_postfix: nil, is_optional: false)
179
+ items = []
180
+ hash.each do |key, value|
181
+ items << TextItemToCheck.new(value, item_name, "#{friendly_name_postfix}: (#{key})", is_optional)
182
+ end
183
+ return items
184
+ end
185
+ end
186
+ end
187
+
188
+ # we want to get some of the same behavior hashes has, so use this mixin specifically designed for Spaceship::Tunes::LanguageItem
189
+ # because we use .each
190
+ module LanguageItemHashBehavior
191
+ # this is used to create a hash-like .each method.
192
+ def each(&block)
193
+ keys.each { |key| yield(key, get_value(key: key)) }
194
+ end
195
+ end
196
+
197
+ class Spaceship::Tunes::LanguageItem
198
+ include LanguageItemHashBehavior
199
+ end