solara 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.
Files changed (175) hide show
  1. checksums.yaml +7 -0
  2. data/bin/solara +18 -0
  3. data/solara/lib/.DS_Store +0 -0
  4. data/solara/lib/core/.DS_Store +0 -0
  5. data/solara/lib/core/aliases/alias_generator.rb +128 -0
  6. data/solara/lib/core/aliases/alias_generator_manager.rb +28 -0
  7. data/solara/lib/core/aliases/solara_terminal_setup.rb +103 -0
  8. data/solara/lib/core/brands/brand_onboarder.rb +46 -0
  9. data/solara/lib/core/brands/brand_switcher.rb +204 -0
  10. data/solara/lib/core/brands/brands_manager.rb +154 -0
  11. data/solara/lib/core/dashboard/.DS_Store +0 -0
  12. data/solara/lib/core/dashboard/brand/.DS_Store +0 -0
  13. data/solara/lib/core/dashboard/brand/BrandDetail.js +11 -0
  14. data/solara/lib/core/dashboard/brand/BrandDetailController.js +361 -0
  15. data/solara/lib/core/dashboard/brand/BrandDetailModel.js +155 -0
  16. data/solara/lib/core/dashboard/brand/BrandDetailView.js +245 -0
  17. data/solara/lib/core/dashboard/brand/brand.html +477 -0
  18. data/solara/lib/core/dashboard/brand/source/BrandLocalSource.js +123 -0
  19. data/solara/lib/core/dashboard/brand/source/BrandRemoteSource.js +260 -0
  20. data/solara/lib/core/dashboard/brands/Brands.js +10 -0
  21. data/solara/lib/core/dashboard/brands/BrandsController.js +155 -0
  22. data/solara/lib/core/dashboard/brands/BrandsModel.js +136 -0
  23. data/solara/lib/core/dashboard/brands/BrandsView.js +136 -0
  24. data/solara/lib/core/dashboard/brands/brands.html +345 -0
  25. data/solara/lib/core/dashboard/component/AddFieldSheet.js +212 -0
  26. data/solara/lib/core/dashboard/component/AliasesBottomSheet.js +128 -0
  27. data/solara/lib/core/dashboard/component/BrandOptionsBottomSheet.js +130 -0
  28. data/solara/lib/core/dashboard/component/ConfirmationDialog.js +103 -0
  29. data/solara/lib/core/dashboard/component/MessageBottomSheet.js +80 -0
  30. data/solara/lib/core/dashboard/component/OnboardBrandBottomSheet.js +214 -0
  31. data/solara/lib/core/dashboard/dashboard_manager.rb +19 -0
  32. data/solara/lib/core/dashboard/dashboard_server.rb +132 -0
  33. data/solara/lib/core/dashboard/handler/base_handler.rb +25 -0
  34. data/solara/lib/core/dashboard/handler/brand_alisases_handler.rb +33 -0
  35. data/solara/lib/core/dashboard/handler/brand_configurations_handler.rb +18 -0
  36. data/solara/lib/core/dashboard/handler/brand_configurations_manager.rb +73 -0
  37. data/solara/lib/core/dashboard/handler/brand_icon_handler.rb +20 -0
  38. data/solara/lib/core/dashboard/handler/brand_section_handler.rb +20 -0
  39. data/solara/lib/core/dashboard/handler/brands_handler.rb +14 -0
  40. data/solara/lib/core/dashboard/handler/current_brand_handler.rb +18 -0
  41. data/solara/lib/core/dashboard/handler/doctor_handler.rb +39 -0
  42. data/solara/lib/core/dashboard/handler/edit_section_handler.rb +55 -0
  43. data/solara/lib/core/dashboard/handler/offboard_brand_handler.rb +34 -0
  44. data/solara/lib/core/dashboard/handler/onboard_brand_handler.rb +53 -0
  45. data/solara/lib/core/dashboard/handler/redirect_handler.rb +12 -0
  46. data/solara/lib/core/dashboard/handler/switch_handler.rb +25 -0
  47. data/solara/lib/core/dashboard/index.html +36 -0
  48. data/solara/lib/core/dashboard/local.html +41 -0
  49. data/solara/lib/core/dashboard/res/favicon/android-chrome-192x192.png +0 -0
  50. data/solara/lib/core/dashboard/res/favicon/android-chrome-512x512.png +0 -0
  51. data/solara/lib/core/dashboard/res/favicon/apple-touch-icon.png +0 -0
  52. data/solara/lib/core/dashboard/res/favicon/favicon-16x16.png +0 -0
  53. data/solara/lib/core/dashboard/res/favicon/favicon-32x32.png +0 -0
  54. data/solara/lib/core/dashboard/res/favicon/favicon.ico +0 -0
  55. data/solara/lib/core/dashboard/res/favicon/site.webmanifest +1 -0
  56. data/solara/lib/core/dashboard/solara.png +0 -0
  57. data/solara/lib/core/doctor/brand_doctor.rb +94 -0
  58. data/solara/lib/core/doctor/doctor_manager.rb +35 -0
  59. data/solara/lib/core/doctor/project_doctor.rb +8 -0
  60. data/solara/lib/core/doctor/schema/brand_configurations.json +60 -0
  61. data/solara/lib/core/doctor/schema/platform/android/android_config.json +23 -0
  62. data/solara/lib/core/doctor/schema/platform/android/android_signing.json +23 -0
  63. data/solara/lib/core/doctor/schema/platform/ios/ios_config.json +27 -0
  64. data/solara/lib/core/doctor/schema/platform/ios/ios_signing.json +27 -0
  65. data/solara/lib/core/doctor/schema/platform/shared/theme.json +48 -0
  66. data/solara/lib/core/doctor/validator/brand_settings_validator.rb +55 -0
  67. data/solara/lib/core/doctor/validator/brand_settings_validator_manager.rb +82 -0
  68. data/solara/lib/core/doctor/validator/directory_structure_validator.rb +38 -0
  69. data/solara/lib/core/doctor/validator/file_structure_validator.rb +37 -0
  70. data/solara/lib/core/doctor/validator/json_file_validator.rb +21 -0
  71. data/solara/lib/core/doctor/validator/json_schema_validator.rb +32 -0
  72. data/solara/lib/core/doctor/validator/project_filesystem_validator.rb +70 -0
  73. data/solara/lib/core/doctor/validator/template/android_template_validation_config.yml +51 -0
  74. data/solara/lib/core/doctor/validator/template/flutter_template_validation_config.yml +53 -0
  75. data/solara/lib/core/doctor/validator/template/ios_template_validation_config.yml +51 -0
  76. data/solara/lib/core/doctor/validator/template/template_validator.rb +108 -0
  77. data/solara/lib/core/doctor/validator/validation_strategy.rb +7 -0
  78. data/solara/lib/core/scripts/brand_config_generator.rb +245 -0
  79. data/solara/lib/core/scripts/brand_config_manager.rb +90 -0
  80. data/solara/lib/core/scripts/brand_exporter.rb +38 -0
  81. data/solara/lib/core/scripts/brand_importer.rb +84 -0
  82. data/solara/lib/core/scripts/brand_offboarder.rb +19 -0
  83. data/solara/lib/core/scripts/brand_resources_manager.rb +77 -0
  84. data/solara/lib/core/scripts/directory_creator.rb +22 -0
  85. data/solara/lib/core/scripts/file_manager.rb +90 -0
  86. data/solara/lib/core/scripts/file_path.rb +327 -0
  87. data/solara/lib/core/scripts/folder_copier.rb +41 -0
  88. data/solara/lib/core/scripts/gitignore_manager.rb +54 -0
  89. data/solara/lib/core/scripts/interactive_file_system_validator.rb +110 -0
  90. data/solara/lib/core/scripts/platform/android/android_manifest_switcher.rb +24 -0
  91. data/solara/lib/core/scripts/platform/android/android_strings_switcher.rb +39 -0
  92. data/solara/lib/core/scripts/platform/android/gradle_switcher.rb +233 -0
  93. data/solara/lib/core/scripts/platform/android/properties_generator.rb +31 -0
  94. data/solara/lib/core/scripts/platform/ios/ios_file_path_manager.rb +109 -0
  95. data/solara/lib/core/scripts/platform/ios/ios_plist_manager.rb +42 -0
  96. data/solara/lib/core/scripts/platform/ios/xcconfig_generator.rb +44 -0
  97. data/solara/lib/core/scripts/platform/ios/xcode_asset_manager.rb +56 -0
  98. data/solara/lib/core/scripts/platform/ios/xcode_project_manager.rb +82 -0
  99. data/solara/lib/core/scripts/platform/ios/xcode_project_switcher.rb +130 -0
  100. data/solara/lib/core/scripts/project_settings_manager.rb +39 -0
  101. data/solara/lib/core/scripts/solara_logger.rb +103 -0
  102. data/solara/lib/core/scripts/solara_settings_manager.rb +73 -0
  103. data/solara/lib/core/scripts/solara_status_manager.rb +55 -0
  104. data/solara/lib/core/scripts/solara_version_manager.rb +42 -0
  105. data/solara/lib/core/scripts/strings_xml_manager.rb +22 -0
  106. data/solara/lib/core/scripts/terminal_input_manager.rb +22 -0
  107. data/solara/lib/core/scripts/theme_generator.rb +250 -0
  108. data/solara/lib/core/scripts/yaml_manager.rb +72 -0
  109. data/solara/lib/core/solara_configurator.rb +15 -0
  110. data/solara/lib/core/template/brands/android/android_config.json +8 -0
  111. data/solara/lib/core/template/brands/android/android_signing.json +6 -0
  112. data/solara/lib/core/template/brands/android/res/.DS_Store +0 -0
  113. data/solara/lib/core/template/brands/android/res/mipmap-hdpi/ic_launcher.png +0 -0
  114. data/solara/lib/core/template/brands/android/res/mipmap-hdpi/ic_launcher_round.png +0 -0
  115. data/solara/lib/core/template/brands/android/res/mipmap-mdpi/ic_launcher.png +0 -0
  116. data/solara/lib/core/template/brands/android/res/mipmap-mdpi/ic_launcher_round.png +0 -0
  117. data/solara/lib/core/template/brands/android/res/mipmap-xhdpi/ic_launcher.png +0 -0
  118. data/solara/lib/core/template/brands/android/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
  119. data/solara/lib/core/template/brands/android/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  120. data/solara/lib/core/template/brands/android/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  121. data/solara/lib/core/template/brands/android/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  122. data/solara/lib/core/template/brands/android/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  123. data/solara/lib/core/template/brands/brands.json +4 -0
  124. data/solara/lib/core/template/brands/ios/assets/.DS_Store +0 -0
  125. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/100.png +0 -0
  126. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/102.png +0 -0
  127. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/1024.png +0 -0
  128. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/114.png +0 -0
  129. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/120.png +0 -0
  130. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/128.png +0 -0
  131. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/144.png +0 -0
  132. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/152.png +0 -0
  133. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/16.png +0 -0
  134. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/167.png +0 -0
  135. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/172.png +0 -0
  136. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/180.png +0 -0
  137. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/196.png +0 -0
  138. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/20.png +0 -0
  139. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/216.png +0 -0
  140. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/256.png +0 -0
  141. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/29.png +0 -0
  142. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/32.png +0 -0
  143. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/40.png +0 -0
  144. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/48.png +0 -0
  145. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/50.png +0 -0
  146. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/512.png +0 -0
  147. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/55.png +0 -0
  148. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/57.png +0 -0
  149. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/58.png +0 -0
  150. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/60.png +0 -0
  151. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/64.png +0 -0
  152. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/66.png +0 -0
  153. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/72.png +0 -0
  154. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/76.png +0 -0
  155. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/80.png +0 -0
  156. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/87.png +0 -0
  157. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/88.png +0 -0
  158. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/92.png +0 -0
  159. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/Contents.json +1 -0
  160. data/solara/lib/core/template/brands/ios/ios_config.json +7 -0
  161. data/solara/lib/core/template/brands/ios/ios_signing.json +7 -0
  162. data/solara/lib/core/template/brands/shared/.DS_Store +0 -0
  163. data/solara/lib/core/template/brands/shared/brand_config.json +2 -0
  164. data/solara/lib/core/template/brands/shared/theme.json +46 -0
  165. data/solara/lib/core/template/config/android_template_config.json +57 -0
  166. data/solara/lib/core/template/config/flutter_template_config.json +62 -0
  167. data/solara/lib/core/template/config/ios_template_config.json +57 -0
  168. data/solara/lib/core/template/project_template_generator.rb +63 -0
  169. data/solara/lib/platform_detector.rb +84 -0
  170. data/solara/lib/solara/cli.rb +5 -0
  171. data/solara/lib/solara/version.rb +3 -0
  172. data/solara/lib/solara.rb +238 -0
  173. data/solara/lib/solara_initializer.rb +44 -0
  174. data/solara/lib/solara_manager.rb +73 -0
  175. metadata +346 -0
@@ -0,0 +1,53 @@
1
+ structure:
2
+ android:
3
+ type: directory
4
+ contents:
5
+ assets:
6
+ type: directory
7
+ res:
8
+ type: directory
9
+ android_config.json:
10
+ type: file
11
+ validations:
12
+ - type: valid_json
13
+ - type: json_schema
14
+ schema_path: platform/android/android_config.json
15
+ android_signing.json:
16
+ type: file
17
+ validations:
18
+ - type: valid_json
19
+ - type: json_schema
20
+ schema_path: platform/android/android_signing.json
21
+ ios:
22
+ type: directory
23
+ contents:
24
+ assets:
25
+ type: directory
26
+ ios_config.json:
27
+ type: file
28
+ validations:
29
+ - type: valid_json
30
+ - type: json_schema
31
+ schema_path: platform/ios/ios_config.json
32
+ ios_signing.json:
33
+ type: file
34
+ validations:
35
+ - type: valid_json
36
+ - type: json_schema
37
+ schema_path: platform/ios/ios_signing.json
38
+ shared:
39
+ type: directory
40
+ contents:
41
+ assets:
42
+ type: directory
43
+ brand_config.json:
44
+ type: file
45
+ validations:
46
+ - type: valid_json
47
+ theme.json:
48
+ type: file
49
+ validations:
50
+ - type: valid_json
51
+ - type: json_schema
52
+ schema_path: platform/shared/theme.json
53
+ strict: false
@@ -0,0 +1,51 @@
1
+ structure:
2
+ android:
3
+ type: directory
4
+ contents:
5
+ assets:
6
+ type: directory
7
+ res:
8
+ type: directory
9
+ android_config.json:
10
+ type: file
11
+ validations:
12
+ - type: valid_json
13
+ - type: json_schema
14
+ schema_path: platform/android/android_config.json
15
+ android_signing.json:
16
+ type: file
17
+ validations:
18
+ - type: valid_json
19
+ - type: json_schema
20
+ schema_path: platform/android/android_signing.json
21
+ ios:
22
+ type: directory
23
+ contents:
24
+ assets:
25
+ type: directory
26
+ ios_config.json:
27
+ type: file
28
+ validations:
29
+ - type: valid_json
30
+ - type: json_schema
31
+ schema_path: platform/ios/ios_config.json
32
+ ios_signing.json:
33
+ type: file
34
+ validations:
35
+ - type: valid_json
36
+ - type: json_schema
37
+ schema_path: platform/ios/ios_signing.json
38
+ shared:
39
+ type: directory
40
+ contents:
41
+ brand_config.json:
42
+ type: file
43
+ validations:
44
+ - type: valid_json
45
+ theme.json:
46
+ type: file
47
+ validations:
48
+ - type: valid_json
49
+ - type: json_schema
50
+ schema_path: platform/shared/theme.json
51
+ strict: false
@@ -0,0 +1,108 @@
1
+ Dir.glob("#{__dir__}/../../doctor/validator/*.rb").each { |file| require file }
2
+ require 'yaml'
3
+ require 'pathname'
4
+ require 'json'
5
+ require 'json-schema'
6
+
7
+ class TemplateValidator
8
+ def initialize(project_path, config_file)
9
+ @project_path = Pathname.new(project_path)
10
+ @config = YAML.load_file(config_file)
11
+ end
12
+
13
+ def validate
14
+ errors = []
15
+ validate_structure(errors, @project_path, @config['structure'])
16
+ errors
17
+ end
18
+
19
+ private
20
+
21
+ def validate_structure(errors, current_path, expected_structure)
22
+ expected_structure.each do |name, details|
23
+ path = current_path.join(name)
24
+
25
+ if details.is_a?(Hash)
26
+ case details['type']
27
+ when 'directory'
28
+ validate_directory(errors, path, details)
29
+ when 'file'
30
+ validate_file(errors, path, details)
31
+ else
32
+ errors << "Unknown type '#{details['type']}' for #{path.relative_path_from(@project_path)}"
33
+ end
34
+ end
35
+ end
36
+
37
+ validate_no_extra_files(errors, current_path, expected_structure) if @config['strict']
38
+ end
39
+
40
+ def validate_directory(errors, path, details)
41
+ unless path.directory?
42
+ errors << "Missing directory: #{path.relative_path_from(@project_path)}"
43
+ return
44
+ end
45
+ validate_structure(errors, path, details['contents']) if details['contents']
46
+ end
47
+
48
+ def validate_file(errors, path, details)
49
+ unless path.file?
50
+ errors << "Missing file: #{path.relative_path_from(@project_path)}"
51
+ return
52
+ end
53
+
54
+ content = File.read(path)
55
+
56
+ details['validations']&.each do |validation|
57
+ case validation['type']
58
+ when 'content_includes'
59
+ unless content.include?(validation['value'])
60
+ errors << "File #{path.relative_path_from(@project_path)} does not contain expected content: #{validation['value']}"
61
+ end
62
+
63
+ when 'content_matches'
64
+ unless content.match?(Regexp.new(validation['value']))
65
+ errors << "File #{path.relative_path_from(@project_path)} does not match expected pattern: #{validation['value']}"
66
+ end
67
+
68
+ when 'file_size'
69
+ size = File.size(path)
70
+ min_size = validation['min_size']
71
+ max_size = validation['max_size']
72
+ if min_size && size < min_size
73
+ errors << "File #{path.relative_path_from(@project_path)} is smaller than expected: #{size} < #{min_size} bytes"
74
+ end
75
+ if max_size && size > max_size
76
+ errors << "File #{path.relative_path_from(@project_path)} is larger than expected: #{size} > #{max_size} bytes"
77
+ end
78
+
79
+ when 'valid_json'
80
+ begin
81
+ JsonFileValidator.new([path]).validate(@project_path)
82
+ rescue StandardError => e
83
+ errors << e.message
84
+ end
85
+
86
+ when 'json_schema'
87
+ begin
88
+ schema_path = File.join(FilePath.schema, validation['schema_path'])
89
+ JsonSchemaValidator.new(schema_path, path).validate(@project_path)
90
+ rescue StandardError => e
91
+ errors << e.message
92
+ end
93
+
94
+ else
95
+ errors << "Unknown validation type '#{validation['type']}' for #{path.relative_path_from(@project_path)}"
96
+ end
97
+ end
98
+ end
99
+
100
+ def validate_no_extra_files(errors, current_path, expected_structure)
101
+ expected_names = expected_structure.keys
102
+ current_path.children.each do |child|
103
+ unless expected_names.include?(child.basename.to_s)
104
+ errors << "Unexpected #{child.directory? ? 'directory' : 'file'}: #{child.relative_path_from(@project_path)}"
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,7 @@
1
+ class ValidationStrategy
2
+ class ValidationError < StandardError; end
3
+
4
+ def validate(project_path)
5
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
6
+ end
7
+ end
@@ -0,0 +1,245 @@
1
+ module Language
2
+ Kotlin = 'kotlin'
3
+ Swift = 'swift'
4
+ Dart = 'dart'
5
+
6
+ def self.all
7
+ [Kotlin, Swift, Dart]
8
+ end
9
+ end
10
+
11
+ def init(language)
12
+ if Language.all.include?(language)
13
+ # Do something with the valid language
14
+ else
15
+ raise ArgumentError, "Invalid language. Please use one of: #{Language.all.join(', ')}"
16
+ end
17
+ end
18
+
19
+ class BrandConfigGenerator
20
+ def initialize(config, output_dir, language, platform)
21
+ @output_dir = output_dir
22
+ @language = language
23
+ @platform = platform
24
+ @config = config
25
+ end
26
+
27
+ def generate
28
+ content = generate_config_content
29
+ FileManager.create_file_if_not_exist(@output_dir)
30
+ File.write(@output_dir, content)
31
+ Solara.logger.debug("Generated brand config #{@output_dir} for: #{@language}")
32
+ end
33
+
34
+ private
35
+
36
+ def generate_config_content
37
+ content = "// Generated by Solara\n"
38
+ content += language_specific_imports
39
+
40
+ classes = []
41
+ generate_classes("BrandConfig", @config, classes)
42
+
43
+ classes.reverse_each do |class_content|
44
+ content += class_content
45
+ content += "\n"
46
+ end
47
+
48
+ content
49
+ end
50
+
51
+ def language_specific_imports
52
+ case @language
53
+ when 'dart'
54
+ "import 'package:flutter/material.dart';\n\n"
55
+ when 'kotlin'
56
+ "import android.graphics.Color\n\n"
57
+ when 'swift'
58
+ "import UIKit\n\n"
59
+ end
60
+ end
61
+
62
+ def generate_classes(class_name, config, classes)
63
+ content = class_declaration(class_name)
64
+ constructor_params = []
65
+
66
+ config.each do |key, value|
67
+ type = value_type(value, key)
68
+ content += property_declaration(key, type)
69
+ constructor_params << constructor_parameter(key, type)
70
+ end
71
+
72
+ content += constructor_declaration(class_name, constructor_params)
73
+ content += instance_declaration(class_name, config)
74
+ content += class_closing
75
+
76
+ classes << content
77
+
78
+ config.each do |key, value|
79
+ if value.is_a?(Hash)
80
+ nested_class_name = "#{key.capitalize}"
81
+ generate_classes(nested_class_name, value, classes)
82
+ end
83
+ end
84
+ end
85
+
86
+ def class_declaration(class_name)
87
+ case @language
88
+ when 'dart'
89
+ "class #{class_name} {\n"
90
+ when 'kotlin'
91
+ "data class #{class_name}(\n"
92
+ when 'swift'
93
+ "struct #{class_name} {\n"
94
+ end
95
+ end
96
+
97
+ def property_declaration(key, type)
98
+ case @language
99
+ when 'dart'
100
+ " final #{type} #{key};\n"
101
+ when 'kotlin'
102
+ " val #{key}: #{type},\n"
103
+ when 'swift'
104
+ " let #{key}: #{type}\n"
105
+ end
106
+ end
107
+
108
+ def constructor_parameter(key, type)
109
+ case @language
110
+ when 'dart'
111
+ "required this.#{key}"
112
+ when 'kotlin'
113
+ "val #{key}: #{type}"
114
+ when 'swift'
115
+ "#{key}: #{type}"
116
+ end
117
+ end
118
+
119
+ def constructor_declaration(class_name, params)
120
+ case @language
121
+ when 'dart'
122
+ params.empty? ? "\n const #{class_name}();\n\n" : "\n const #{class_name}({#{params.join(', ')}});\n\n"
123
+ when 'kotlin'
124
+ ") {\n"
125
+ when 'swift'
126
+ "\n init(#{params.join(', ')}) {\n#{params.map { |p| " self.#{p.split(':').first} = #{p.split(':').first}" }.join("\n")}\n }\n\n"
127
+ end
128
+ end
129
+
130
+ def instance_declaration(class_name, config)
131
+ case @language
132
+ when 'dart'
133
+ " static const #{class_name} instance = #{class_name}(\n#{config.map { |k, v| " #{k}: #{value_for(v, k, ' ')}" }.join(",\n")}\n );\n"
134
+ when 'kotlin'
135
+ "companion object {\n val instance = #{class_name}(\n#{config.map { |k, v| " #{k} = #{value_for(v, k, ' ')}" }.join(",\n")}\n )\n}\n"
136
+ when 'swift'
137
+ " static let shared = #{class_name}(\n#{config.map { |k, v| " #{k}: #{value_for(v, k, ' ')}" }.join(",\n")}\n )\n"
138
+ end
139
+ end
140
+
141
+ def class_closing
142
+ @language == 'swift' ? "}\n" : "}\n"
143
+ end
144
+
145
+ def value_type(value, class_prefix)
146
+ case value
147
+ when String then language_specific_type('String')
148
+ when Integer then language_specific_type('Int')
149
+ when Float then language_specific_type('Double')
150
+ when TrueClass, FalseClass then language_specific_type('Bool')
151
+ when Array
152
+ if value.all? { |item| item.is_a?(String) }
153
+ language_specific_type('Array<String>')
154
+ elsif value.all? { |item| item.is_a?(Integer) }
155
+ language_specific_type('Array<Int>')
156
+ elsif value.all? { |item| item.is_a?(Float) }
157
+ language_specific_type('Array<Double>')
158
+ elsif value.all? { |item| item.is_a?(TrueClass) || item.is_a?(FalseClass) }
159
+ language_specific_type('Array<Bool>')
160
+ else
161
+ language_specific_type('Array<Any>')
162
+ end
163
+ when Hash then "#{class_prefix.capitalize}"
164
+ else
165
+ language_specific_type('Any')
166
+ end
167
+ end
168
+
169
+ def language_specific_type(type)
170
+ case @language
171
+ when 'dart'
172
+ case type
173
+ when 'String' then 'String'
174
+ when 'Int' then 'int'
175
+ when 'Double' then 'double'
176
+ when 'Bool' then 'bool'
177
+ when 'Array<String>' then 'List<String>'
178
+ when 'Array<Int>' then 'List<int>'
179
+ when 'Array<Double>' then 'List<double>'
180
+ when 'Array<Bool>' then 'List<bool>'
181
+ when 'Array<Any>' then 'List<dynamic>'
182
+ when 'Any' then 'dynamic'
183
+ else type
184
+ end
185
+ when 'kotlin'
186
+ case type
187
+ when 'Array<String>' then 'List<String>'
188
+ when 'Array<Int>' then 'List<Int>'
189
+ when 'Array<Double>' then 'List<Double>'
190
+ when 'Array<Bool>' then 'List<Boolean>'
191
+ when 'Array<Any>' then 'List<Any>'
192
+ when 'Any' then 'Any'
193
+ else type
194
+ end
195
+ when 'swift'
196
+ type
197
+ end
198
+ end
199
+
200
+ def value_for(value, class_prefix, indent)
201
+ case value
202
+ when String
203
+ if value.start_with?('#') && value.length == 7 # Assume it's a color
204
+ color_for(value)
205
+ else
206
+ value.inspect
207
+ end
208
+ when Integer, Float
209
+ value.to_s
210
+ when TrueClass, FalseClass
211
+ value.to_s
212
+ when Array
213
+ array_items = value.map { |item| value_for(item, class_prefix, indent + ' ') }.join(', ')
214
+ "[\n#{indent} #{array_items}\n#{indent}]"
215
+ when Hash
216
+ "#{class_prefix.capitalize}.instance"
217
+ else
218
+ language_specific_null
219
+ end
220
+ end
221
+
222
+ def color_for(value)
223
+ case @language
224
+ when 'dart'
225
+ "Color(0xFF#{value[1..-1]})"
226
+ when 'kotlin'
227
+ "Color.parseColor(\"#{value}\")"
228
+ when 'swift'
229
+ "UIColor(red: #{hex_to_rgb(value[:red])}, green: #{hex_to_rgb(value[:green])}, blue: #{hex_to_rgb(value[:blue])}, alpha: 1.0)"
230
+ end
231
+ end
232
+
233
+ def hex_to_rgb(hex)
234
+ hex.to_i(16) / 255.0
235
+ end
236
+
237
+ def language_specific_null
238
+ case @language
239
+ when 'dart', 'swift'
240
+ 'null'
241
+ when 'kotlin'
242
+ 'null'
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,90 @@
1
+ Dir.glob("#{__dir__}/../scripts/platform/android/*.rb").each { |file| require file }
2
+ Dir.glob("#{__dir__}/../scripts/platform/ios/*.rb").each { |file| require file }
3
+
4
+ class BrandConfigManager
5
+ def initialize(brand_key)
6
+ @brand_key = brand_key
7
+ end
8
+
9
+ def generate_brand_config(
10
+ name,
11
+ language,
12
+ platform)
13
+ Solara.logger.start_step("Generate #{language} brand config for #{platform}")
14
+ config = load_config(FilePath.brand_config(@brand_key))
15
+ add_basic_brand_info(config, platform)
16
+ config_generator = BrandConfigGenerator.new(
17
+ config,
18
+ FilePath.generated_config(name, platform),
19
+ language,
20
+ platform
21
+ )
22
+ config_generator.generate
23
+ Solara.logger.end_step("Generate #{language} brand config for #{platform}")
24
+ end
25
+
26
+ def generate_android_properties
27
+ generator = PropertiesGenerator.new(@brand_key)
28
+ generator.generate
29
+ end
30
+
31
+ def generate_ios_xcconfig
32
+ generator = XcconfigGenerator.new(@brand_key)
33
+ generator.generate
34
+ end
35
+
36
+ private
37
+
38
+ def add_basic_brand_info(config, platform)
39
+ case platform
40
+ when Platform::Flutter
41
+ add_android_info(config, prefix: 'android')
42
+ add_ios_info(config, prefix: 'iOS')
43
+ when Platform::Android
44
+ add_android_info(config)
45
+ when Platform::IOS
46
+ add_ios_info(config)
47
+ else
48
+ raise ArgumentError, "Invalid platform: #{@platform}"
49
+ end
50
+ end
51
+
52
+ def add_android_info(config, prefix: '')
53
+ android_config = load_config(FilePath.android_config(@brand_key))
54
+ config_mappings = {
55
+ 'brandName' => 'BrandName',
56
+ 'applicationId' => 'ApplicationId',
57
+ 'versionName' => 'VersionName',
58
+ 'versionCode' => 'VersionCode'
59
+ }
60
+
61
+ add_config_info(config, android_config, config_mappings, prefix)
62
+ end
63
+
64
+ def add_ios_info(config, prefix: '')
65
+ ios_config = load_config(FilePath.ios_config(@brand_key))
66
+ config_mappings = {
67
+ 'PRODUCT_NAME' => 'ProductName',
68
+ 'PRODUCT_BUNDLE_IDENTIFIER' => 'BundleIdentifier',
69
+ 'MARKETING_VERSION' => 'MarketingVersion',
70
+ 'BUNDLE_VERSION' => 'BundleVersion'
71
+ }
72
+
73
+ add_config_info(config, ios_config, config_mappings, prefix)
74
+ end
75
+
76
+ def add_config_info(config, source_config, mappings, prefix)
77
+ mappings.each do |source_key, target_key|
78
+ key = if prefix.empty?
79
+ target_key[0].downcase + target_key[1..]
80
+ else
81
+ "#{prefix}#{target_key}"
82
+ end
83
+ config[key] = source_config[source_key]
84
+ end
85
+ end
86
+
87
+ def load_config(file_path)
88
+ JSON.parse(File.read(file_path))
89
+ end
90
+ end
@@ -0,0 +1,38 @@
1
+ require 'json'
2
+ require 'fileutils'
3
+
4
+ class BrandExporter
5
+
6
+ def start(brand_keys, path)
7
+ if !path.nil? && !path.strip.empty? && !File.directory?(path)
8
+ Solara.logger.failure("#{path} is not a directory. Please specify a valid directory.")
9
+ return
10
+ end
11
+
12
+ directory = path || FilePath.project_root
13
+
14
+ brand_keys.each do |brand_key|
15
+ export(brand_key, directory)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def export(brand_key, directory)
22
+ if BrandsManager.instance.find(brand_key).nil?
23
+ Solara.logger.failure("#{brand_key} doesn't exist!")
24
+ return
25
+ end
26
+
27
+ brand = BrandsManager.instance.brand_with_configurations(brand_key)
28
+ json = JSON.pretty_generate(brand)
29
+ json_file_path = File.join(directory, "#{brand_key}-solara-configurations.json")
30
+
31
+ # Create the file if it does not exist
32
+ File.open(json_file_path, 'w') do |file|
33
+ file.write(json)
34
+ end
35
+
36
+ Solara.logger.success("Successfully exported brand #{brand_key} to: #{json_file_path}")
37
+ end
38
+ end
@@ -0,0 +1,84 @@
1
+ require 'json'
2
+ require 'fileutils'
3
+
4
+ class BrandImporter
5
+
6
+ def start(configurations_paths)
7
+
8
+ configurations_paths.each do |path|
9
+ import(path)
10
+ end
11
+
12
+ end
13
+
14
+ private
15
+
16
+ def import(configurations_path)
17
+ unless File.exist?(configurations_path)
18
+ Solara.logger.failure("#{configurations_path} doesn't exist!")
19
+ return
20
+ end
21
+
22
+ unless File.file?(configurations_path)
23
+ Solara.logger.failure("#{configurations_path} is not configurations file!")
24
+ return
25
+ end
26
+
27
+ validate_json(configurations_path)
28
+ validate_json_schema(configurations_path)
29
+
30
+ configurations_json = JSON.parse(File.read(configurations_path))
31
+ brand = configurations_json['brand']
32
+ brand_key = brand['key'] # Ensure to use 'key' instead of 'brand_key'
33
+
34
+ exists = BrandsManager.instance.exists(brand_key)
35
+ unless exists
36
+ SolaraManager.new.onboard(brand_key, brand['name'], open_dashboard: false)
37
+ end
38
+
39
+ update_brand(brand_key, configurations_json)
40
+
41
+ message_suffix = exists ? "The existing brand '#{brand_key}' has been updated." : "A new brand with the key '#{brand_key}' has been onboarded."
42
+ Solara.logger.success("Successfully imported (#{configurations_path}). #{message_suffix}")
43
+ end
44
+
45
+ def update_brand(brand_key, configurations_json)
46
+ brand_path = FilePath.brand(brand_key)
47
+
48
+ configurations_json['configurations'].each do |configuration|
49
+ file_name = "#{configuration['key']}.json"
50
+ file_path = find_file_in_subdirectories(brand_path, file_name)
51
+
52
+ # Create or replace the contents of the configuration file
53
+ if file_path
54
+ File.write(file_path, JSON.pretty_generate(configuration['content']))
55
+ else
56
+ Solara.logger.failure("File #{file_name} not found in #{brand_path}, ignoring it!")
57
+ end
58
+ end
59
+ end
60
+
61
+ def find_file_in_subdirectories(base_path, file_name)
62
+ Dir.glob(File.join(base_path, '**', file_name)).first
63
+ end
64
+
65
+ def validate_json(configurations_path)
66
+ begin
67
+ JsonFileValidator.new([configurations_path]).validate
68
+ rescue StandardError => e
69
+ Solara.logger.fatal(e.message)
70
+ exit 1
71
+ end
72
+ end
73
+
74
+ def validate_json_schema(configurations_path)
75
+ begin
76
+ schema_path = FilePath.brand_configurations_schema
77
+ JsonSchemaValidator.new(schema_path, configurations_path).validate
78
+ rescue StandardError => e
79
+ Solara.logger.fatal(e.message)
80
+ exit 1
81
+ end
82
+ end
83
+
84
+ end