scarpe 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (240) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -0
  3. data/.yardopts +11 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +112 -0
  6. data/README.md +31 -24
  7. data/Rakefile +13 -1
  8. data/docs/yard/catscradle.md +44 -0
  9. data/docs/yard/template/default/fulldoc/html/setup.rb +13 -0
  10. data/docs/yard/template/default/layout/html/setup.rb +9 -0
  11. data/examples/background_with_image.rb +16 -0
  12. data/examples/bloopsaphone/working/bronx_army_knife.rb +66 -0
  13. data/examples/bloopsaphone/working/morning_serenity.rb +21 -0
  14. data/examples/bloopsaphone/working/simpsons_theme_song_by_why.rb +6 -4
  15. data/examples/button_go_away.rb +1 -1
  16. data/examples/check.rb +18 -0
  17. data/examples/clear_and_append.rb +24 -0
  18. data/examples/download_and_show_image.rb +28 -0
  19. data/examples/edit_box.rb +3 -5
  20. data/examples/fonts.rb +2 -2
  21. data/examples/get_headers.rb +10 -0
  22. data/examples/highlander.rb +2 -0
  23. data/examples/link.rb +2 -2
  24. data/examples/local_fonts.rb +4 -0
  25. data/examples/local_images.rb +4 -0
  26. data/examples/motion_events.rb +20 -0
  27. data/examples/parse_xl_funnies.rb +58 -0
  28. data/examples/radio/radio.rb +16 -0
  29. data/examples/radio/radio_groups.rb +18 -0
  30. data/examples/radio/radio_same_slot.rb +6 -0
  31. data/examples/ruby_racer.rb +13 -15
  32. data/examples/selfitude.rb +18 -0
  33. data/examples/shapes/shapes_fill.rb +4 -3
  34. data/examples/shoes_school.rb +2 -4
  35. data/examples/show_hide.rb +6 -0
  36. data/examples/skip_ci/change_my_audio_source.rb +21 -0
  37. data/examples/skip_ci/guitar_fretboard.rb +137 -0
  38. data/examples/video.rb +10 -0
  39. data/exe/scarpe +42 -66
  40. data/fonts/Pacifico.ttf +0 -0
  41. data/lacci/Gemfile +22 -0
  42. data/lacci/Gemfile.lock +72 -0
  43. data/lacci/Rakefile +12 -0
  44. data/lacci/lacci.gemspec +37 -0
  45. data/lacci/lib/lacci/scarpe_cli.rb +70 -0
  46. data/lacci/lib/lacci/scarpe_core.rb +21 -0
  47. data/lacci/lib/lacci/version.rb +13 -0
  48. data/lacci/lib/shoes/app.rb +264 -0
  49. data/{lib/scarpe → lacci/lib/shoes}/background.rb +1 -1
  50. data/{lib/scarpe → lacci/lib/shoes}/border.rb +1 -1
  51. data/{lib/scarpe → lacci/lib/shoes}/colors.rb +1 -1
  52. data/lacci/lib/shoes/constants.rb +29 -0
  53. data/{lib/scarpe → lacci/lib/shoes}/display_service.rb +40 -45
  54. data/lacci/lib/shoes/download.rb +123 -0
  55. data/lacci/lib/shoes/log.rb +71 -0
  56. data/lacci/lib/shoes/spacing.rb +9 -0
  57. data/{lib/scarpe → lacci/lib/shoes}/widget.rb +63 -43
  58. data/{lib/scarpe → lacci/lib/shoes/widgets}/alert.rb +3 -3
  59. data/{lib/scarpe → lacci/lib/shoes/widgets}/arc.rb +7 -5
  60. data/{lib/scarpe → lacci/lib/shoes/widgets}/button.rb +3 -3
  61. data/lacci/lib/shoes/widgets/check.rb +28 -0
  62. data/lacci/lib/shoes/widgets/document_root.rb +20 -0
  63. data/{lib/scarpe → lacci/lib/shoes/widgets}/edit_box.rb +10 -5
  64. data/{lib/scarpe → lacci/lib/shoes/widgets}/edit_line.rb +2 -2
  65. data/lacci/lib/shoes/widgets/flow.rb +22 -0
  66. data/lacci/lib/shoes/widgets/font.rb +14 -0
  67. data/{lib/scarpe → lacci/lib/shoes/widgets}/image.rb +3 -7
  68. data/lacci/lib/shoes/widgets/line.rb +18 -0
  69. data/{lib/scarpe → lacci/lib/shoes/widgets}/link.rb +2 -2
  70. data/{lib/scarpe → lacci/lib/shoes/widgets}/list_box.rb +2 -2
  71. data/{lib/scarpe → lacci/lib/shoes/widgets}/para.rb +4 -26
  72. data/lacci/lib/shoes/widgets/radio.rb +35 -0
  73. data/lacci/lib/shoes/widgets/shape.rb +37 -0
  74. data/lacci/lib/shoes/widgets/slot.rb +75 -0
  75. data/{lib/scarpe → lacci/lib/shoes/widgets}/span.rb +2 -2
  76. data/lacci/lib/shoes/widgets/stack.rb +24 -0
  77. data/{lib/scarpe → lacci/lib/shoes/widgets}/star.rb +6 -9
  78. data/lacci/lib/shoes/widgets/subscription_item.rb +60 -0
  79. data/lacci/lib/shoes/widgets/text_widget.rb +51 -0
  80. data/lacci/lib/shoes/widgets/video.rb +15 -0
  81. data/lacci/lib/shoes/widgets.rb +29 -0
  82. data/lacci/lib/shoes.rb +127 -0
  83. data/lacci/test/test_colors.rb +39 -0
  84. data/lacci/test/test_helper.rb +9 -0
  85. data/lacci/test/test_lacci.rb +9 -0
  86. data/lib/scarpe/cats_cradle.rb +249 -0
  87. data/lib/scarpe/evented_assertions.rb +88 -0
  88. data/lib/scarpe/version.rb +1 -1
  89. data/lib/scarpe/wv/alert.rb +3 -2
  90. data/lib/scarpe/wv/app.rb +30 -8
  91. data/lib/scarpe/wv/arc.rb +5 -6
  92. data/lib/scarpe/wv/background.rb +10 -1
  93. data/lib/scarpe/wv/border.rb +5 -3
  94. data/lib/scarpe/wv/button.rb +11 -9
  95. data/lib/scarpe/wv/check.rb +29 -0
  96. data/lib/scarpe/wv/control_interface.rb +14 -20
  97. data/lib/scarpe/wv/control_interface_test.rb +13 -28
  98. data/lib/scarpe/wv/document_root.rb +3 -45
  99. data/lib/scarpe/wv/edit_box.rb +5 -7
  100. data/lib/scarpe/wv/edit_line.rb +2 -2
  101. data/lib/scarpe/wv/flow.rb +10 -20
  102. data/lib/scarpe/wv/font.rb +36 -0
  103. data/lib/scarpe/wv/html.rb +3 -2
  104. data/lib/scarpe/wv/image.rb +7 -2
  105. data/lib/scarpe/wv/line.rb +4 -7
  106. data/lib/scarpe/wv/link.rb +1 -0
  107. data/lib/scarpe/wv/list_box.rb +3 -3
  108. data/lib/scarpe/wv/para.rb +16 -14
  109. data/lib/scarpe/wv/radio.rb +34 -0
  110. data/lib/scarpe/wv/shape.rb +44 -8
  111. data/lib/scarpe/wv/slot.rb +81 -0
  112. data/lib/scarpe/wv/spacing.rb +1 -1
  113. data/lib/scarpe/wv/span.rb +10 -8
  114. data/lib/scarpe/wv/stack.rb +10 -30
  115. data/lib/scarpe/wv/star.rb +11 -12
  116. data/lib/scarpe/wv/subscription_item.rb +50 -0
  117. data/lib/scarpe/wv/video.rb +34 -0
  118. data/lib/scarpe/wv/web_wrangler.rb +238 -58
  119. data/lib/scarpe/wv/webview_local_display.rb +27 -5
  120. data/lib/scarpe/wv/webview_relay_display.rb +18 -119
  121. data/lib/scarpe/wv/webview_relay_util.rb +143 -0
  122. data/lib/scarpe/wv/widget.rb +80 -11
  123. data/lib/scarpe/wv/wv_display_worker.rb +17 -4
  124. data/lib/scarpe/wv.rb +33 -4
  125. data/lib/scarpe/wv_local.rb +1 -1
  126. data/lib/scarpe/wv_relay.rb +1 -1
  127. data/lib/scarpe.rb +3 -32
  128. data/scarpe-components/.gitignore +1 -0
  129. data/scarpe-components/Gemfile +22 -0
  130. data/scarpe-components/README.md +35 -0
  131. data/scarpe-components/Rakefile +12 -0
  132. data/scarpe-components/lib/scarpe/components/base64.rb +29 -0
  133. data/scarpe-components/lib/scarpe/components/file_helpers.rb +65 -0
  134. data/scarpe-components/lib/scarpe/components/modular_logger.rb +113 -0
  135. data/scarpe-components/lib/scarpe/components/print_logger.rb +43 -0
  136. data/{lib/scarpe → scarpe-components/lib/scarpe/components}/promises.rb +102 -35
  137. data/scarpe-components/lib/scarpe/components/segmented_file_loader.rb +170 -0
  138. data/scarpe-components/lib/scarpe/components/unit_test_helpers.rb +217 -0
  139. data/scarpe-components/lib/scarpe/components/version.rb +7 -0
  140. data/scarpe-components/scarpe-components.gemspec +38 -0
  141. data/scarpe-components/test/test_components.rb +9 -0
  142. data/scarpe-components/test/test_helper.rb +23 -0
  143. data/scarpe-components/test/test_promises.rb +260 -0
  144. data/scarpe-components/test/test_segmented_app_files.rb +182 -0
  145. data/{lib/scarpe → spikes}/glibui/widget.rb +2 -2
  146. data/{lib/scarpe → spikes}/glibui.rb +1 -1
  147. data/templates/basic_class_template.erb +1 -1
  148. data/templates/class_template_with_event_bind.erb +1 -1
  149. data/templates/class_template_with_shapes.erb +1 -1
  150. data/templates/webview_template.erb +0 -3
  151. metadata +151 -118
  152. data/examples/fill.rb +0 -25
  153. data/examples/legacy/not_checked/shoes-contrib/basic/class-book.yaml +0 -387
  154. data/examples/legacy/not_checked/shoes-contrib/good/good-clock.rb +0 -51
  155. data/examples/legacy/not_checked/shoes-contrib/good/good-follow.rb +0 -26
  156. data/examples/legacy/not_checked/shoes-contrib/good/good-reminder.rb +0 -174
  157. data/examples/legacy/not_checked/shoes-contrib/good/good-vjot.rb +0 -56
  158. data/examples/legacy/not_checked/shoes-contrib/simple/simple-timer.rb +0 -13
  159. data/examples/legacy/not_checked/shoes-dep-samples/good-clock.rb +0 -51
  160. data/examples/legacy/not_checked/shoes-dep-samples/good-follow.rb +0 -26
  161. data/examples/legacy/not_checked/shoes-dep-samples/good-reminder.rb +0 -174
  162. data/examples/legacy/not_checked/shoes-dep-samples/good-vjot.rb +0 -56
  163. data/examples/legacy/not_checked/shoes-dep-samples/simple-accordion.rb +0 -75
  164. data/examples/legacy/not_checked/shoes-dep-samples/simple-anim-shapes.rb +0 -17
  165. data/examples/legacy/not_checked/shoes-dep-samples/simple-anim-text.rb +0 -13
  166. data/examples/legacy/not_checked/shoes-dep-samples/simple-arc.rb +0 -23
  167. data/examples/legacy/not_checked/shoes-dep-samples/simple-bounce.rb +0 -24
  168. data/examples/legacy/not_checked/shoes-dep-samples/simple-calc.rb +0 -70
  169. data/examples/legacy/not_checked/shoes-dep-samples/simple-chipmunk.rb +0 -26
  170. data/examples/legacy/not_checked/shoes-dep-samples/simple-control-sizes.rb +0 -24
  171. data/examples/legacy/not_checked/shoes-dep-samples/simple-curve.rb +0 -26
  172. data/examples/legacy/not_checked/shoes-dep-samples/simple-dialogs.rb +0 -29
  173. data/examples/legacy/not_checked/shoes-dep-samples/simple-draw.rb +0 -13
  174. data/examples/legacy/not_checked/shoes-dep-samples/simple-editor.rb +0 -28
  175. data/examples/legacy/not_checked/shoes-dep-samples/simple-form.rb +0 -28
  176. data/examples/legacy/not_checked/shoes-dep-samples/simple-form.shy +0 -0
  177. data/examples/legacy/not_checked/shoes-dep-samples/simple-mask.rb +0 -21
  178. data/examples/legacy/not_checked/shoes-dep-samples/simple-menu.rb +0 -31
  179. data/examples/legacy/not_checked/shoes-dep-samples/simple-menu1.rb +0 -35
  180. data/examples/legacy/not_checked/shoes-dep-samples/simple-rubygems.rb +0 -29
  181. data/examples/legacy/not_checked/shoes-dep-samples/simple-slide.rb +0 -45
  182. data/examples/legacy/not_checked/shoes-dep-samples/simple-sphere.rb +0 -28
  183. data/examples/legacy/not_checked/shoes-dep-samples/simple-sqlite3.rb +0 -13
  184. data/examples/legacy/not_checked/shoes-dep-samples/simple-timer.rb +0 -13
  185. data/examples/legacy/not_checked/shoes-dep-samples/simple-video.rb +0 -13
  186. data/examples/legacy/not_checked/simple/anim-text.rb +0 -13
  187. data/examples/legacy/not_checked/simple/arc.rb +0 -23
  188. data/examples/legacy/not_checked/simple/bounce.rb +0 -24
  189. data/examples/legacy/not_checked/simple/chipmunk.rb +0 -26
  190. data/examples/legacy/not_checked/simple/curve.rb +0 -26
  191. data/examples/legacy/not_checked/simple/dialogs.rb +0 -29
  192. data/examples/legacy/not_checked/simple/downloader.rb +0 -40
  193. data/examples/legacy/not_checked/simple/draw.rb +0 -13
  194. data/examples/legacy/not_checked/simple/mask.rb +0 -21
  195. data/examples/legacy/not_checked/simple/slide.rb +0 -45
  196. data/examples/legacy/not_checked/simple/sphere.rb +0 -28
  197. data/lib/constants.rb +0 -5
  198. data/lib/scarpe/app.rb +0 -78
  199. data/lib/scarpe/document_root.rb +0 -20
  200. data/lib/scarpe/fill.rb +0 -23
  201. data/lib/scarpe/flow.rb +0 -19
  202. data/lib/scarpe/line.rb +0 -25
  203. data/lib/scarpe/logger.rb +0 -155
  204. data/lib/scarpe/shape.rb +0 -19
  205. data/lib/scarpe/spacing.rb +0 -9
  206. data/lib/scarpe/stack.rb +0 -70
  207. data/lib/scarpe/text_widget.rb +0 -42
  208. data/lib/scarpe/unit_test_helpers.rb +0 -163
  209. data/lib/scarpe/widgets.rb +0 -30
  210. data/lib/scarpe/wv/fill.rb +0 -30
  211. data/lib/scarpe/wv/shape_helper.rb +0 -44
  212. data/scarpe-0.2.0.gem +0 -0
  213. /data/{lib/scarpe → spikes}/glibui/README.md +0 -0
  214. /data/{lib/scarpe → spikes}/glibui/alert.rb +0 -0
  215. /data/{lib/scarpe → spikes}/glibui/app.rb +0 -0
  216. /data/{lib/scarpe → spikes}/glibui/background.rb +0 -0
  217. /data/{lib/scarpe → spikes}/glibui/border.rb +0 -0
  218. /data/{lib/scarpe → spikes}/glibui/button.rb +0 -0
  219. /data/{lib/scarpe → spikes}/glibui/dimensions.rb +0 -0
  220. /data/{lib/scarpe → spikes}/glibui/document_root.rb +0 -0
  221. /data/{lib/scarpe → spikes}/glibui/edit_box.rb +0 -0
  222. /data/{lib/scarpe → spikes}/glibui/edit_line.rb +0 -0
  223. /data/{lib/scarpe → spikes}/glibui/flow.rb +0 -0
  224. /data/{lib/scarpe → spikes}/glibui/html.rb +0 -0
  225. /data/{lib/scarpe → spikes}/glibui/image.rb +0 -0
  226. /data/{lib/scarpe → spikes}/glibui/link.rb +0 -0
  227. /data/{lib/scarpe → spikes}/glibui/local_display.rb +0 -0
  228. /data/{lib/scarpe → spikes}/glibui/para.rb +0 -0
  229. /data/{lib/scarpe → spikes}/glibui/spacing.rb +0 -0
  230. /data/{lib/scarpe → spikes}/glibui/stack.rb +0 -0
  231. /data/{lib/scarpe → spikes}/glibui/text_widget.rb +0 -0
  232. /data/{lib/scarpe → spikes}/libui/alert.rb +0 -0
  233. /data/{lib/scarpe → spikes}/libui/button.rb +0 -0
  234. /data/{lib/scarpe → spikes}/libui/colors.rb +0 -0
  235. /data/{lib/scarpe → spikes}/libui/core.rb +0 -0
  236. /data/{lib/scarpe → spikes}/libui/flow.rb +0 -0
  237. /data/{lib/scarpe → spikes}/libui/libui.rb +0 -0
  238. /data/{lib/scarpe → spikes}/libui/notepad.md +0 -0
  239. /data/{lib/scarpe → spikes}/libui/para.rb +0 -0
  240. /data/{lib/scarpe → spikes}/libui/stack.rb +0 -0
data/lib/scarpe.rb CHANGED
@@ -1,42 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "scarpe/logger"
4
-
5
- # This will never be triggered -- we use the (...) feature below, which means this
6
- # file won't even parse in old Rubies.
7
3
  if RUBY_VERSION[0..2] < "3.2"
8
- Scarpe::Logger.logger("Scarpe").error("Scarpe requires Ruby 3.2 or higher!")
4
+ Shoes::Log.logger("Scarpe").error("Scarpe requires Ruby 3.2 or higher!")
9
5
  exit(-1)
10
6
  end
11
7
 
12
- require "securerandom"
13
- require "json"
14
-
15
- require_relative "constants"
16
-
17
- class Scarpe::Error < StandardError; end
18
-
19
- require_relative "scarpe/version"
20
- require_relative "scarpe/promises"
21
- require_relative "scarpe/display_service"
22
- require_relative "scarpe/widgets"
23
-
24
- require "bloops"
8
+ require "shoes"
9
+ require "lacci/scarpe_core"
25
10
 
26
11
  d_s = ENV["SCARPE_DISPLAY_SERVICE"] || "wv_local"
27
12
  # This is require, not require_relative, to allow gems to supply a new display service
28
13
  require "scarpe/#{d_s}"
29
-
30
- #Constants Module
31
- include Constants
32
-
33
- class Scarpe
34
- class << self
35
- def app(...)
36
- app = Scarpe::App.new(...)
37
- app.init
38
- app.run
39
- app.destroy
40
- end
41
- end
42
- end
@@ -0,0 +1 @@
1
+ scarpe-components-*.gem
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ gem "lacci", path: "../lacci"
8
+
9
+ gem "rake", "~> 13.0"
10
+
11
+ gem "nokogiri"
12
+
13
+ group :test do
14
+ gem "minitest", "~> 5.0"
15
+ gem "minitest-reporters"
16
+ end
17
+
18
+ group :development do
19
+ gem "debug"
20
+ gem "rubocop", "~> 1.21"
21
+ gem "rubocop-shopify"
22
+ end
@@ -0,0 +1,35 @@
1
+ # Scarpe-Components
2
+
3
+ Scarpe is several things. We package up the Shoes API with as few implementation details as possible (called Lacci.) We have a Webview-based display library, in both local and over-a-socket (relay) versions. We have a Wasm display library (Scarpe-Wasm) in a separate gem.
4
+
5
+ And we have various default implementations of different reusable components. Scarpe's hierarchical logger isn't particularly specific to your display library or underlying GUI library, but we'd like it to be usable by multiple display libs. CatsCradle is only useful if you're trying to fix up an impedance mismatch between Ruby and an evented under-layer, but it's not really specific to Webview. Scarpe's default downloader, using Ruby's built-in HTTP libraries, is good in many cases but you might want to replace it (e.g. with Typhoeus for better parallel downloads or Hystrix for robustness to bad network connections) in some cases.
6
+
7
+ Part of our solution is the Scarpe-Components gem, which lives in the same repository as Scarpe (for now?). These components can live there. Any specific display library can pick and choose what it wants or needs and handle its dependencies as it sees fit.
8
+
9
+ ## Dependency Hell
10
+
11
+ A "gem full of optional reusable components" presents an awkward challenge for Rubygems. What are the gem's dependencies? Do you make it depend on every possible library, requiring FastImage and Nokogiri and SQLite and whatever else any component might ever require? That would be as bad as making ActiveRecord need MySQL and SQLite and Postgres and Oracle and... But if you don't give it *any* dependencies, you're going to have a harsh surprise at runtime when Bundler can't find any of the gems you need.
12
+
13
+ You can break everything up into tiny pieces, like ActiveRecord having a separate adapter for certain databases. You could do the same with an activerecord-mysql and activerecord-postgres and activerecord-sqlite gem to complete the set. Or, like, ActiveRecord, you can say "declare activerecord as a dependency, and also one or more other database gems, and we'll figure out what's available at runtime." That approach can be pretty fragile and it adds extra complexity -- how do you make sure everything is required at the right time and ActiveRecord can find everything that's available? How do you balance gem dependencies that are built into ActiveRecord with those for unusual databases that get added by other gems, later? Your plugin system (like ActiveRecord's) can get extensive and fragile.
14
+
15
+ Scarpe-Components kicks that problem down the road to the display libraries. Would a particular display library like to use the FastImage-based image implementation? Great! It can declare a dependency on the FastImage gem. Would it like to optionally allow the built-in Ruby downloader, but also have an optional robustified version using RestClient? Lovely. It can either create multiple gems (e.g. scarpe-gtk-rcdownload and scarpe-gtk-plaindownload) or look for RestClient being available at runtime, as it pleases. The FastImage implementation lives in Scarpe-Components, but the dependency is declared by the display library or the app.
16
+
17
+ ## Using an Implementation
18
+
19
+ Components in Scarpe-Components are designed to be used individually. They can require each other, but they should only require components they will actually use. The whole library is designed to be used a la carte, and normally no display service will use every component.
20
+
21
+ A display library will declare scarpe-components as a dependency. Then, usually from its named require file (e.g. wv_local.rb, wv_relay.rb, wasm_local.rb) it will set up those dependencies by requiring components that it wants and if necessary creating and configuring them. That way a specific display service (e.g. wv_local) can require a specific component (e.g. Logging-gem-based hierarchical logging) and configure it how it wants (e.g. ENV var pointing to a local JSON config file.)
22
+
23
+ ## How is this Different from Lacci?
24
+
25
+ Scarpe already has a gem full of reusable components, one that every Scarpe-based application already has to use. Lacci declares the Shoes API, and is 100% required for every Scarpe-based Shoes application everywhere.
26
+
27
+ So how do you tell what goes into Scarpe-Components vs what goes into Lacci?
28
+
29
+ Lacci is, at heart, an API with as little implementation as possible. It declares the Shoes GUI objects, but not how to display them. Ordinarily a Shoes application will require (in the sense of Kernel#require) every part of Lacci -- it will load the entire library. Even if we add optional components (e.g. a Lacci-based Bloops API with no implementation behind it), those components will be loaded by every Shoes app that uses that part of the API.
30
+
31
+ As a result, Lacci should have minimal (preferably zero) dependencies. Any code doing "real work" should be removed from Lacci completely, as soon as possible. Since *every* Shoes app is going to require Lacci, it should be possible to use it with essentially no dependencies, minimal memory footprint, minimal load time. It is a skeleton to hang functionality on, not a thing that functions for itself.
32
+
33
+ Scarpe-Components is a grab bag of default implementations, intended to be replaceable. But each of them does something. Most of them have dependencies. It's fine for a Scarpe-Component implementation to depend on the Logging gem, provided it says it does. Nokogiri? 100% fair. Does it do something with a nontrivial amount of computation, like rendering HTML output? No problem. This is very different from Lacci.
34
+
35
+ If a component should be reused, it's probably fine to put it into Scarpe-Components. It *might* be fine to put it in Lacci. For Scarpe-Components, ask yourself, "will more than one Scarpe display service possibly want to use this?" For Lacci, the test is more strict: "will *every* Scarpe display service want to use this? Does it have no dependencies at all, and do very little computation and take very little memory?"
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/test_*.rb"]
10
+ end
11
+
12
+ task default: [:test]
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "uri"
5
+
6
+ class Scarpe; end
7
+ module Scarpe::Components; end
8
+ class Scarpe
9
+ module Components::Base64
10
+ def valid_url?(string)
11
+ uri = URI.parse(string)
12
+ uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
13
+ rescue URI::InvalidURIError, URI::BadURIError
14
+ false
15
+ end
16
+
17
+ def encode_file_to_base64(image_filename)
18
+ directory_path = File.dirname(__FILE__, 5)
19
+
20
+ image_path = File.join(directory_path, image_filename)
21
+
22
+ image_data = File.binread(image_path)
23
+
24
+ encoded_data = ::Base64.strict_encode64(image_data)
25
+
26
+ encoded_data
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tempfile"
4
+
5
+ # These can be used for unit tests, but also more generally.
6
+
7
+ module Scarpe::Components::FileHelpers
8
+ # Create a temporary file with the given prefix and contents.
9
+ # Execute the block of code with it in place. Make sure
10
+ # it gets cleaned up afterward.
11
+ #
12
+ # @param prefix [String] the prefix passed to Tempfile to identify this file on disk
13
+ # @param contents [String] the file contents that should be written to Tempfile
14
+ # @param dir [String] the directory to create the tempfile in
15
+ # @yield The code to execute with the tempfile present
16
+ # @yieldparam the path of the new tempfile
17
+ def with_tempfile(prefix, contents, dir: Dir.tmpdir)
18
+ t = Tempfile.new(prefix, dir)
19
+ t.write(contents)
20
+ t.flush # Make sure the contents are written out
21
+
22
+ yield(t.path)
23
+ ensure
24
+ t.close
25
+ t.unlink
26
+ end
27
+
28
+ # Create multiple tempfiles, with given contents, in given
29
+ # directories, and execute the block in that context.
30
+ # When the block is finished, make sure all tempfiles are
31
+ # deleted.
32
+ #
33
+ # Pass an array of arrays, where each array is of the form:
34
+ # [prefix, contents, (optional)dir]
35
+ #
36
+ # I don't love inlining with_tempfile's contents into here.
37
+ # But calling it iteratively or recursively was difficult
38
+ # when I tried it the obvious ways.
39
+ #
40
+ # This method should be equivalent to calling with_tempfile
41
+ # once for each entry in the array, in a set of nested
42
+ # blocks.
43
+ #
44
+ # @param tf_specs [Array<Array>] The array of tempfile prefixes, contents and directories
45
+ # @yield The code to execute with those tempfiles present
46
+ # @yieldparam An array of paths to tempfiles, in the same order as tf_specs
47
+ def with_tempfiles(tf_specs, &block)
48
+ tempfiles = []
49
+ tf_specs.each do |prefix, contents, dir|
50
+ dir ||= Dir.tmpdir
51
+ t = Tempfile.new(prefix, dir)
52
+ tempfiles << t
53
+ t.write(contents)
54
+ t.flush # Make sure the contents are written out
55
+ end
56
+
57
+ args = tempfiles.map(&:path)
58
+ yield(args)
59
+ ensure
60
+ tempfiles.each do |t|
61
+ t.close
62
+ t.unlink
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logging"
4
+ require "json"
5
+
6
+ require "shoes/log"
7
+
8
+ # Requires the logging gem
9
+
10
+ class Scarpe; end
11
+ module Scarpe::Components; end
12
+ class Scarpe
13
+ class Components::ModularLogImpl
14
+ include Shoes::Log # for constants
15
+
16
+ def logger_for_component(component)
17
+ Logging.logger[component]
18
+ end
19
+
20
+ private
21
+
22
+ def name_to_severity(data)
23
+ case data
24
+ when "debug"
25
+ :debug
26
+ when "info"
27
+ :info
28
+ when "warn"
29
+ :warn
30
+ when "err", "error"
31
+ :error
32
+ when "fatal"
33
+ :fatal
34
+ else
35
+ raise "Don't know how to treat #{data.inspect} as a logger severity!"
36
+ end
37
+ end
38
+
39
+ def json_to_appender(data)
40
+ case data.downcase
41
+ when "stdout"
42
+ Logging.appenders.stdout layout: @custom_log_layout
43
+ when "stderr"
44
+ Logging.appenders.stderr layout: @custom_log_layout
45
+ when String
46
+ Logging.appenders.file data, layout: @custom_log_layout
47
+ else
48
+ raise "Don't know how to convert #{data.inspect} to an appender!"
49
+ end
50
+ end
51
+
52
+ def json_configure_logger(logger, data)
53
+ case data
54
+ in String
55
+ sev = name_to_severity(data)
56
+ logger.level = sev
57
+ in [level, *locations]
58
+ if logger.name != "root"
59
+ # The Logging gem doesn't have an additive property on the root logger
60
+ logger.additive = false # Don't also log to parent/root loggers
61
+ end
62
+
63
+ logger.appenders = locations.map { |where| json_to_appender(where) }
64
+
65
+ logger.level = name_to_severity(level)
66
+ else
67
+ raise "Don't know how to use #{data.inspect} to specify a logger!"
68
+ end
69
+ end
70
+
71
+ def freeze_log_config(log_config)
72
+ log_config.each do |k, v|
73
+ k.freeze
74
+ v.freeze
75
+ v.each(&:freeze) if v.is_a?(Array)
76
+ end
77
+ log_config.freeze
78
+ end
79
+
80
+ public
81
+
82
+ def configure_logger(log_config)
83
+ # TODO: custom coloring? https://github.com/TwP/logging/blob/master/examples/colorization.rb
84
+ @custom_log_layout = Logging.layouts.pattern pattern: '[%r] %-5l %c: %m\n'
85
+
86
+ if log_config.is_a?(String) && File.exist?(log_config)
87
+ log_config = JSON.load_file(log_config)
88
+ end
89
+
90
+ log_config = freeze_log_config(log_config) unless log_config.nil?
91
+ @current_log_config = log_config # Save a copy for later
92
+
93
+ Logging.reset # Reset all Logging settings to defaults
94
+ Logging.reopen # For log-reconfig (e.g. test failures), often important to *not* store an open handle to a moved file
95
+ return if log_config.nil?
96
+
97
+ Logging.logger.root.appenders = [Logging.appenders.stdout]
98
+
99
+ default_logger = log_config[DEFAULT_COMPONENT] || "info"
100
+ json_configure_logger(Logging.logger.root, default_logger)
101
+
102
+ log_config.each do |component, logger_data|
103
+ next if component == DEFAULT_COMPONENT
104
+
105
+ sublogger = Logging.logger[component]
106
+ json_configure_logger(sublogger, logger_data)
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ #Shoes::Log.instance = Scarpe::Components::ModularLogImpl.new
113
+ #Shoes::Log.configure_logger(Shoes::Log::DEFAULT_LOG_CONFIG)
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shoes/log"
4
+ require "json"
5
+
6
+ class Scarpe; end
7
+ module Scarpe::Components; end
8
+ class Scarpe::Components::PrintLogImpl
9
+ include Shoes::Log # for constants
10
+
11
+ class PrintLogger
12
+ def initialize(component_name)
13
+ @comp_name = component_name
14
+ end
15
+
16
+ def error(msg)
17
+ puts "#{@comp_name} error: #{msg}"
18
+ end
19
+
20
+ def warn(msg)
21
+ puts "#{@comp_name} warn: #{msg}"
22
+ end
23
+
24
+ def debug(msg)
25
+ puts "#{@comp_name} debug: #{msg}"
26
+ end
27
+
28
+ def info(msg)
29
+ puts "#{@comp_name} info: #{msg}"
30
+ end
31
+ end
32
+
33
+ def logger_for_component(component)
34
+ PrintLogger.new(component.to_s)
35
+ end
36
+
37
+ def configure_logger(log_config)
38
+ # For now, ignore
39
+ end
40
+ end
41
+
42
+ #Shoes::Log.instance = Scarpe::PrintLogImpl.new
43
+ #Shoes::Log.configure_logger(Shoes::Log::DEFAULT_LOG_CONFIG)
@@ -1,42 +1,56 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Funny thing... We need promises as an API concept since we have a JS event
4
- # loop doing its thing, and we need to respond to actions that it takes.
5
- # But there's not really a Ruby implementation of Promises *without* an
6
- # attached form of concurrency. So here we are, writing our own :-/
7
- #
8
- # In theory you could probably write some kind of "no-op thread pool"
9
- # for the ruby-concurrency gem, pass it manually to every promise we
10
- # created and then raise an exception any time we tried to do something
11
- # in the background. That's probably more code than writing our own, though,
12
- # and we'd be fighting it constantly.
13
- #
14
- # This class is inspired by concurrent-ruby [Promise](https://ruby-concurrency.github.io/concurrent-ruby/1.1.5/Concurrent/Promise.html)
15
- # which is inspired by Javascript Promises, which is what we actually need
16
- # for our use case. We can't easily tell when our WebView begins processing
17
- # our request, which removes the :processing state. This can be used for
18
- # executing JS, but also generally waiting on events.
19
- #
20
- # We don't fully control ordering here, so it *is* conceivable that a
21
- # child waiting on a parent can be randomly fulfilled, even if we didn't
22
- # expect it. We don't consider that an error. Similarly, we'll call
23
- # on_scheduled callbacks if a promise is fulfilled, even though we
24
- # never explicitly scheduled it. If a promise is *rejected* without
25
- # ever being scheduled, we won't call those callbacks.
26
-
3
+ class Scarpe; end
4
+ module Scarpe::Components; end
27
5
  class Scarpe
6
+ # Scarpe::Promise is a promises library, but one with no form of built-in
7
+ # concurrency. Instead, promise callbacks are executed synchronously.
8
+ # Even execution is usually synchronous, but can also be handled manually
9
+ # for forms of execution not controlled in Ruby (like Webview.)
10
+ #
11
+ # Funny thing... We need promises as an API concept since we have a JS event
12
+ # loop doing its thing, and we need to respond to actions that it takes.
13
+ # But there's not really a Ruby implementation of Promises *without* an
14
+ # attached form of concurrency. So here we are, writing our own :-/
15
+ #
16
+ # In theory you could probably write some kind of "no-op thread pool"
17
+ # for the ruby-concurrency gem, pass it manually to every promise we
18
+ # created and then raise an exception any time we tried to do something
19
+ # in the background. That's probably more code than writing our own, though,
20
+ # and we'd be fighting it constantly.
21
+ #
22
+ # This class is inspired by concurrent-ruby [Promise](https://ruby-concurrency.github.io/concurrent-ruby/1.1.5/Concurrent/Promise.html)
23
+ # which is inspired by Javascript Promises, which is what we actually need
24
+ # for our use case. We can't easily tell when our WebView begins processing
25
+ # our request, which removes the :processing state. This can be used for
26
+ # executing JS, but also generally waiting on events.
27
+ #
28
+ # We don't fully control ordering here, so it *is* conceivable that a
29
+ # child waiting on a parent can be randomly fulfilled, even if we didn't
30
+ # expect it. We don't consider that an error. Similarly, we'll call
31
+ # on_scheduled callbacks if a promise is fulfilled, even though we
32
+ # never explicitly scheduled it. If a promise is *rejected* without
33
+ # ever being scheduled, we won't call those callbacks.
28
34
  class Promise
29
- include Scarpe::Log
35
+ include Shoes::Log
30
36
 
37
+ # The unscheduled promise state means it's waiting on a parent promise that
38
+ # hasn't completed yet. The pending state means it's waiting to execute.
39
+ # Fulfilled means it has completed successfully and returned a value,
40
+ # while rejected means it has failed, normally producing a reason.
31
41
  PROMISE_STATES = [:unscheduled, :pending, :fulfilled, :rejected]
32
42
 
43
+ # The state of the promise, which should be one of PROMISE_STATES
33
44
  attr_reader :state
45
+
46
+ # The parent promises of this promise, sometimes an empty array
34
47
  attr_reader :parents
48
+
49
+ # If the promise is fulfilled, this is the value returned
35
50
  attr_reader :returned_value
36
- attr_reader :reason
37
51
 
38
- # These methods are meant to be a prettier interface to promises,
39
- # suitable for day-to-day usage.
52
+ # If the promise is rejected, this is the reason, sometimes an exception
53
+ attr_reader :reason
40
54
 
41
55
  # Create a promise and then instantly fulfill it.
42
56
  def self.fulfilled(return_val = nil, parents: [], &block)
@@ -52,19 +66,19 @@ class Scarpe
52
66
  p
53
67
  end
54
68
 
55
- # Instance methods
56
-
69
+ # Fulfill the promise, setting the returned_value to value
57
70
  def fulfilled!(value = nil)
58
71
  set_state(:fulfilled, value)
59
72
  end
60
73
 
74
+ # Reject the promise, setting the reason to reason
61
75
  def rejected!(reason = nil)
62
76
  set_state(:rejected, reason)
63
77
  end
64
78
 
79
+ # Create a new promise with this promise as a parent. It runs the
80
+ # specified code in block when scheduled.
65
81
  def then(&block)
66
- # Create a new promise. It's waiting on us. It runs the
67
- # specified code when scheduled.
68
82
  Promise.new(parents: [self], &block)
69
83
  end
70
84
 
@@ -73,7 +87,15 @@ class Scarpe
73
87
  # the prettiest. However, they ensure that guarantees are made
74
88
  # and so on, so they're great as plumbing under the syntactic
75
89
  # sugar above.
76
-
90
+ #
91
+ # Note that the state passed in may not be the actual initial
92
+ # state. If a parent is rejected, the state will become
93
+ # rejected. If no parents are waiting or failed then a state
94
+ # of nil or :unscheduled will become :pending.
95
+ #
96
+ # @param state [Symbol] One of PROMISE_STATES for the initial state
97
+ # @param parents [Array] A list of promises that must be fulfilled before this one is scheduled
98
+ # @yield A block that executes when this promise is scheduled - when its parents, if any, are all fulfilled
77
99
  def initialize(state: nil, parents: [], &scheduler)
78
100
  log_init("Promise")
79
101
 
@@ -131,15 +153,43 @@ class Scarpe
131
153
  end
132
154
  end
133
155
 
156
+ # Return true if the Promise is either fulfilled or rejected.
157
+ #
158
+ # @return [Boolean] true if the promise is fulfilled or rejected
134
159
  def complete?
135
160
  @state == :fulfilled || @state == :rejected
136
161
  end
137
162
 
163
+ # Return true if the promise is already fulfilled.
164
+ #
165
+ # @return [Boolean] true if the promise is fulfilled
166
+ def fulfilled?
167
+ @state == :fulfilled
168
+ end
169
+
170
+ # Return true if the promise is already rejected.
171
+ #
172
+ # @return [Boolean] true if the promise is rejected
173
+ def rejected?
174
+ @state == :rejected
175
+ end
176
+
177
+ # An inspect method to give slightly smaller output, for ease of reading in irb
178
+ def inspect
179
+ "#<Scarpe::Promise:#{object_id} " +
180
+ "@state=#{@state.inspect} @parents=#{@parents.inspect} " +
181
+ "@waiting_on=#{@waiting_on.inspect} @on_fulfilled=#{@on_fulfilled.size} " +
182
+ "@on_rejected=#{@on_rejected.size} @on_scheduled=#{@on_scheduled.size} " +
183
+ "@scheduler=#{@scheduler ? "Y" : "N"} @executor=#{@executor ? "Y" : "N"} " +
184
+ "@returned_value=#{@returned_value.inspect} @reason=#{@reason.inspect}" +
185
+ ">"
186
+ end
187
+
138
188
  # These promises are mostly designed for external execution.
139
189
  # You could put together your own thread-pool, or use RPC,
140
190
  # a WebView, a database or similar source of external calculation.
141
- # But in some cases, sure, it's reasonable to execute locally.
142
- # In those cases, you can register an executor, which will be
191
+ # But in many cases it's reasonable to execute locally.
192
+ # In those cases, you can register an executor which will be
143
193
  # called when the promise is ready to execute but has not yet
144
194
  # done so. Registering an executor on a promise that is
145
195
  # already fulfilled is an error. Registering an executor on
@@ -332,6 +382,11 @@ class Scarpe
332
382
 
333
383
  public
334
384
 
385
+ # Register a handler to be called when the promise is fulfilled.
386
+ # If called on a fulfilled promise, the handler will be called immediately.
387
+ #
388
+ # @yield Handler to be called on fulfilled
389
+ # @return [Scarpe::Promise] self
335
390
  def on_fulfilled(&handler)
336
391
  unless handler
337
392
  raise "You must pass a block to on_fulfilled!"
@@ -349,6 +404,11 @@ class Scarpe
349
404
  self
350
405
  end
351
406
 
407
+ # Register a handler to be called when the promise is rejected.
408
+ # If called on a rejected promise, the handler will be called immediately.
409
+ #
410
+ # @yield Handler to be called on rejected
411
+ # @return [Scarpe::Promise] self
352
412
  def on_rejected(&handler)
353
413
  unless handler
354
414
  raise "You must pass a block to on_rejected!"
@@ -366,6 +426,12 @@ class Scarpe
366
426
  self
367
427
  end
368
428
 
429
+ # Register a handler to be called when the promise is scheduled.
430
+ # If called on a promise that was scheduled earlier, the handler
431
+ # will be called immediately.
432
+ #
433
+ # @yield Handler to be called on scheduled
434
+ # @return [Scarpe::Promise] self
369
435
  def on_scheduled(&handler)
370
436
  unless handler
371
437
  raise "You must pass a block to on_scheduled!"
@@ -384,4 +450,5 @@ class Scarpe
384
450
  self
385
451
  end
386
452
  end
453
+ Components::Promise = Promise
387
454
  end