ruflet_rails 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 58ab2a08d2694662952c449f89a6c539e01ad118ea38db22eb90b9eb13b4c16a
4
- data.tar.gz: bd95c4b9e1c9794a263ca29d429e035492de1a99148e6f5b5574d085493a3aa7
3
+ metadata.gz: 91f92e0fef72cbb500ba43ac12d81590bac485bb9cf4642c93563aab2736811f
4
+ data.tar.gz: 609e6b2720465ca414898482223d561d264ddca837d717e07a2b54c1f874df6f
5
5
  SHA512:
6
- metadata.gz: d0275388d9faf332d9e6bb84ab629f9fab542133c94c6231e9518bf4b1ffdeb6fdc73519ca9ef861fe1224d5e4e34dafa0ac750d333942ee7b75e9a502d92f70
7
- data.tar.gz: 2653405372a6c783e7996390d8254ae17e7dce914a2066839a1fb924f81c282fca75074a72cdf7094bca2269627b5933d1151149ee9a4218931b4ca0ce2e9b1a
6
+ metadata.gz: 1f9e56edf7b86fccf776d7bfc8ee630ce501f42fde7854a9bb4f660a31ef85dfbe0e1a50ebe7a5c09dfe307e80eb88ca561a77d69777006a00810c34770dbba5
7
+ data.tar.gz: 23792bdae076841ddc0ed099475a25ab4bfd0240a244e90a0d71eb3d011c11c8e67e5610102bf8475a86e35dae2ea2aac6c8f58d1b75c1a8bf23ddf9682da60e
data/README.md CHANGED
@@ -9,26 +9,62 @@ No separate protocol gem is required.
9
9
 
10
10
  ```ruby
11
11
  # Gemfile
12
- gem "ruflet_rails", ">= 0.0.4"
12
+ gem "ruflet_rails", ">= 0.0.5"
13
13
  ```
14
14
 
15
- ```ruby
16
- # app/mobile/main.rb
17
- require "ruflet"
15
+ ## Install into Rails
18
16
 
19
- class MyApp < Ruflet::App
20
- def view(page)
21
- page.title = "Hello"
22
- page.add(text(value: "Hello Ruflet"))
23
- end
24
- end
17
+ ```bash
18
+ bin/rails generate ruflet:install
19
+ ```
25
20
 
26
- MyApp.new.run
21
+ This generator will:
22
+ - create `app/mobile/main.rb`
23
+ - create `ruflet.yaml`
24
+ - add the Ruflet mount route to `config/routes.rb`
25
+ - copy/configure `ruflet_client` when the template is available locally
26
+
27
+ Generated `ruflet.yaml`:
28
+
29
+ ```yaml
30
+ app:
31
+ name: My App
32
+ ruflet_client_url: ""
33
+
34
+ services: []
35
+
36
+ assets:
37
+ splash_screen: assets/splash.png
38
+ icon_launcher: assets/icon.png
27
39
  ```
28
40
 
41
+ For Rails apps, those asset paths are resolved from `app/assets/` during build.
42
+
43
+ ## Build client from Rails
44
+
45
+ Uses the same build pipeline as `ruflet build`:
46
+
47
+ ```bash
48
+ bundle exec rake ruflet:build[web]
49
+ bundle exec rake ruflet:build[macos]
50
+ bundle exec rake ruflet:build[windows]
51
+ bundle exec rake ruflet:build[linux]
52
+ bundle exec rake ruflet:build[apk]
53
+ bundle exec rake ruflet:build[android]
54
+ bundle exec rake ruflet:build[ios]
55
+ bundle exec rake ruflet:build[aab]
56
+ ```
57
+
58
+ ## Manual usage
59
+
29
60
  ```ruby
30
- # config/routes.rb
31
- mount Ruflet::Rails.mobile(Rails.root.join("app/mobile/main.rb")), at: "/ws"
61
+ # app/mobile/main.rb
62
+ require "ruflet"
63
+
64
+ Ruflet.run do |page|
65
+ page.title = "Hello"
66
+ page.add(text("Hello Ruflet"))
67
+ end
32
68
  ```
33
69
 
34
70
  Connect mobile to Rails base URL, e.g. `http://10.0.2.2:3000`.
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "ruflet/rails/install_support"
5
+
6
+ module Ruflet
7
+ module Generators
8
+ class InstallGenerator < ::Rails::Generators::Base
9
+ desc "Install Ruflet into a Rails app."
10
+
11
+ def create_mobile_entrypoint
12
+ target = File.join(destination_root, "app/mobile/main.rb")
13
+ return if File.exist?(target)
14
+
15
+ FileUtils.mkdir_p(File.dirname(target))
16
+ File.write(
17
+ target,
18
+ Ruflet::Rails::InstallSupport.default_mobile_app_template(app_title: app_name)
19
+ )
20
+ end
21
+
22
+ def create_ruflet_yaml
23
+ target = File.join(destination_root, "ruflet.yaml")
24
+ return if File.exist?(target)
25
+
26
+ File.write(
27
+ target,
28
+ Ruflet::Rails::InstallSupport.default_ruflet_yaml(app_name: app_name)
29
+ )
30
+ end
31
+
32
+ def copy_client_template
33
+ copied = Ruflet::Rails::InstallSupport.copy_ruflet_client_template(destination_root)
34
+ say_status(:warn, "ruflet_client template not found; add it manually before building", :yellow) unless copied
35
+ end
36
+
37
+ def configure_client_template
38
+ Ruflet::Rails::InstallSupport.configure_ruflet_client(destination_root)
39
+ end
40
+
41
+ def add_routes
42
+ target = File.join(destination_root, "config/routes.rb")
43
+ return unless File.file?(target)
44
+
45
+ route = Ruflet::Rails::InstallSupport.route_snippet
46
+ return if File.read(target).include?(route)
47
+
48
+ insert_into_file target, " #{route}\n", after: /Rails\.application\.routes\.draw do\s*\n/
49
+ end
50
+
51
+ def print_install_status
52
+ say "ruflet.yaml generated"
53
+ end
54
+
55
+ private
56
+
57
+ def app_name
58
+ File.basename(destination_root).gsub(/[_-]+/, " ").split.map(&:capitalize).join(" ")
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "yaml"
5
+
6
+ module Ruflet
7
+ module Rails
8
+ module InstallSupport
9
+ CLIENT_EXTENSION_MAP = {
10
+ "ads" => { package: "flet_ads", alias: "ruflet_ads" },
11
+ "audio" => { package: "flet_audio", alias: "ruflet_audio" },
12
+ "audio_recorder" => { package: "flet_audio_recorder", alias: "ruflet_audio_recorder" },
13
+ "camera" => { package: "flet_camera", alias: "ruflet_camera" },
14
+ "charts" => { package: "flet_charts", alias: "ruflet_charts" },
15
+ "code_editor" => { package: "flet_code_editor", alias: "ruflet_code_editor" },
16
+ "color_pickers" => { package: "flet_color_pickers", alias: "ruflet_color_picker" },
17
+ "datatable2" => { package: "flet_datatable2", alias: "ruflet_datatable2" },
18
+ "flashlight" => { package: "flet_flashlight", alias: "ruflet_flashlight" },
19
+ "geolocator" => { package: "flet_geolocator", alias: "ruflet_geolocator" },
20
+ "lottie" => { package: "flet_lottie", alias: "ruflet_lottie" },
21
+ "map" => { package: "flet_map", alias: "ruflet_map" },
22
+ "permission_handler" => { package: "flet_permission_handler", alias: "ruflet_permission_handler" },
23
+ "secure_storage" => { package: "flet_secure_storage", alias: "ruflet_secure_storage" },
24
+ "video" => { package: "flet_video", alias: "ruflet_video" },
25
+ "webview" => { package: "flet_webview", alias: "ruflet_webview" }
26
+ }.freeze
27
+
28
+ module_function
29
+
30
+ def default_mobile_app_template(app_title:)
31
+ <<~RUBY
32
+ require "ruflet"
33
+
34
+ Ruflet.run do |page|
35
+ page.title = #{app_title.inspect}
36
+ count = 0
37
+ count_text = text(count.to_s, size: 40)
38
+
39
+ page.add(
40
+ container(
41
+ expand: true,
42
+ alignment: Ruflet::MainAxisAlignment::CENTER,
43
+ content: column(
44
+ alignment: Ruflet::MainAxisAlignment::CENTER,
45
+ horizontal_alignment: Ruflet::CrossAxisAlignment::CENTER,
46
+ children: [
47
+ text("You have pushed the button this many times:"),
48
+ count_text
49
+ ]
50
+ )
51
+ ),
52
+ floating_action_button: fab(
53
+ icon: Ruflet::MaterialIcons::ADD,
54
+ on_click: ->(_e) do
55
+ count += 1
56
+ page.update(count_text, value: count.to_s)
57
+ end
58
+ )
59
+ )
60
+ end
61
+ RUBY
62
+ end
63
+
64
+ def default_ruflet_yaml(app_name:)
65
+ <<~YAML
66
+ app:
67
+ name: #{app_name}
68
+ ruflet_client_url: ""
69
+
70
+ services: []
71
+
72
+ assets:
73
+ splash_screen: assets/splash.png
74
+ icon_launcher: assets/icon.png
75
+ YAML
76
+ end
77
+
78
+ def route_snippet(entrypoint: "app/mobile/main.rb", mount_path: "/ws")
79
+ %(mount Ruflet::Rails.mobile(Rails.root.join("#{entrypoint}")), at: "#{mount_path}")
80
+ end
81
+
82
+ def client_template_root
83
+ env = ENV["RUFLET_CLIENT_TEMPLATE_DIR"].to_s.strip
84
+ return env if !env.empty? && Dir.exist?(env)
85
+
86
+ candidates = [
87
+ File.expand_path("../../../../../ruflet_client", __dir__),
88
+ File.expand_path("../../../../../templates/ruflet_flutter_template", __dir__)
89
+ ]
90
+ candidates.find { |path| Dir.exist?(path) }
91
+ end
92
+
93
+ def copy_ruflet_client_template(root)
94
+ template_root = client_template_root
95
+ return false unless template_root
96
+
97
+ target = File.join(root, "ruflet_client")
98
+ return true if Dir.exist?(target)
99
+
100
+ FileUtils.cp_r(template_root, target)
101
+ prune_client_template(target)
102
+ true
103
+ end
104
+
105
+ def prune_client_template(target)
106
+ %w[
107
+ .dart_tool
108
+ .idea
109
+ build
110
+ ios/Pods
111
+ ios/.symlinks
112
+ ios/Podfile.lock
113
+ macos/Pods
114
+ macos/Podfile.lock
115
+ android/.gradle
116
+ android/.kotlin
117
+ android/local.properties
118
+ ].each do |path|
119
+ full = File.join(target, path)
120
+ FileUtils.rm_rf(full) if File.exist?(full)
121
+ end
122
+ end
123
+
124
+ def configure_ruflet_client(root)
125
+ config_path = File.join(root, "ruflet.yaml")
126
+ return unless File.file?(config_path)
127
+
128
+ config = YAML.safe_load(File.read(config_path), aliases: true) || {}
129
+ extension_keys = Array(config["services"]).map { |v| normalize_extension_key(v) }.compact.uniq
130
+ extension_packages = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:package) }.uniq
131
+ extension_aliases = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:alias) }.uniq
132
+
133
+ client_dir = File.join(root, "ruflet_client")
134
+ return unless Dir.exist?(client_dir)
135
+
136
+ prune_client_pubspec(File.join(client_dir, "pubspec.yaml"), extension_packages)
137
+ prune_client_main(File.join(client_dir, "lib", "main.dart"), extension_aliases)
138
+ end
139
+
140
+ def normalize_extension_key(value)
141
+ key = value.to_s.strip.downcase
142
+ return nil if key.empty?
143
+
144
+ key.tr!("-", "_")
145
+ key.gsub!(/\A(flet_)+/, "")
146
+ key.gsub!(/\Aservice_/, "")
147
+ key.gsub!(/\Acontrol_/, "")
148
+ key = "file_picker" if key == "filepicker"
149
+ key
150
+ end
151
+
152
+ def prune_client_pubspec(path, selected_packages)
153
+ return unless File.file?(path)
154
+
155
+ data = YAML.safe_load(File.read(path), aliases: true) || {}
156
+ deps = (data["dependencies"] || {}).dup
157
+ deps.keys.each do |name|
158
+ next unless name.start_with?("flet_")
159
+ next if name == "flet"
160
+ next if selected_packages.include?(name)
161
+
162
+ deps.delete(name)
163
+ end
164
+ data["dependencies"] = deps
165
+ File.write(path, YAML.dump(data))
166
+ end
167
+
168
+ def prune_client_main(path, selected_aliases)
169
+ return unless File.file?(path)
170
+
171
+ lines = File.readlines(path)
172
+ alias_to_package = {}
173
+
174
+ lines.each do |line|
175
+ match = line.match(%r{\Aimport 'package:(flet_[^/]+)/\1\.dart' as ([a-zA-Z0-9_]+);})
176
+ alias_to_package[match[2]] = match[1] if match
177
+ end
178
+
179
+ kept = lines.select do |line|
180
+ import_match = line.match(%r{\Aimport 'package:(flet_[^/]+)/\1\.dart' as ([a-zA-Z0-9_]+);})
181
+ if import_match
182
+ package_name = import_match[1]
183
+ next true if package_name == "flet"
184
+ next true if selected_aliases.include?(import_match[2])
185
+ next false
186
+ end
187
+
188
+ extension_match = line.match(/\A\s*([a-zA-Z0-9_]+)\.Extension\(\),\s*\z/)
189
+ if extension_match
190
+ extension_alias = extension_match[1]
191
+ package_name = alias_to_package[extension_alias]
192
+ next true if package_name.nil?
193
+ next true if selected_aliases.include?(extension_alias)
194
+ next false
195
+ end
196
+
197
+ true
198
+ end
199
+
200
+ File.write(path, kept.join)
201
+ end
202
+ end
203
+ end
204
+ end
@@ -50,7 +50,7 @@ module Ruflet
50
50
  when Ruflet::Protocol::ACTIONS[:update_control], Ruflet::Protocol::ACTIONS[:update_control_props]
51
51
  on_update_control(ws, payload)
52
52
  when Ruflet::Protocol::ACTIONS[:invoke_control_method]
53
- nil
53
+ on_invoke_control_method(ws, payload)
54
54
  else
55
55
  raise "Unknown action: #{action.inspect}"
56
56
  end
@@ -144,6 +144,11 @@ module Ruflet
144
144
  page.apply_client_update(update["id"], update["props"] || {})
145
145
  end
146
146
 
147
+ def on_invoke_control_method(ws, payload)
148
+ page = fetch_page(ws)
149
+ page.handle_invoke_method_result(Ruflet::Protocol.normalize_invoke_method_result_payload(payload))
150
+ end
151
+
147
152
  def fetch_page(ws)
148
153
  page = @sessions_mutex.synchronize { @sessions[ws.session_key] }
149
154
  raise "Session not found" unless page
@@ -18,7 +18,7 @@ module Ruflet
18
18
  when Float
19
19
  "\xcb".b + [value].pack("G")
20
20
  when String
21
- pack_string(value)
21
+ binary_string?(value) ? pack_binary(value) : pack_string(value)
22
22
  when Symbol
23
23
  pack_string(value.to_s)
24
24
  when Array
@@ -71,6 +71,23 @@ module Ruflet
71
71
  end
72
72
  end
73
73
 
74
+ def pack_binary(value)
75
+ bytes = value.to_s.b
76
+ len = bytes.bytesize
77
+
78
+ if len <= 0xff
79
+ "\xc4".b + [len].pack("C") + bytes
80
+ elsif len <= 0xffff
81
+ "\xc5".b + [len].pack("n") + bytes
82
+ else
83
+ "\xc6".b + [len].pack("N") + bytes
84
+ end
85
+ end
86
+
87
+ def binary_string?(value)
88
+ value.encoding == Encoding::BINARY || !value.valid_encoding?
89
+ end
90
+
74
91
  def pack_array(value)
75
92
  len = value.length
76
93
  head =
@@ -130,10 +147,23 @@ module Ruflet
130
147
  when 0xd9 then reader.read_string(reader.read_u8)
131
148
  when 0xda then reader.read_string(reader.read_u16)
132
149
  when 0xdb then reader.read_string(reader.read_u32)
150
+ when 0xc4 then reader.read_binary(reader.read_u8)
151
+ when 0xc5 then reader.read_binary(reader.read_u16)
152
+ when 0xc6 then reader.read_binary(reader.read_u32)
133
153
  when 0xdc then read_array(reader, reader.read_u16)
134
154
  when 0xdd then read_array(reader, reader.read_u32)
135
155
  when 0xde then read_map(reader, reader.read_u16)
136
156
  when 0xdf then read_map(reader, reader.read_u32)
157
+ when 0xd4
158
+ read_ext(reader, 1)
159
+ when 0xd5
160
+ read_ext(reader, 2)
161
+ when 0xd6
162
+ read_ext(reader, 4)
163
+ when 0xd7
164
+ read_ext(reader, 8)
165
+ when 0xd8
166
+ read_ext(reader, 16)
137
167
  when 0xc7
138
168
  read_ext(reader, reader.read_u8)
139
169
  when 0xc8
@@ -167,8 +197,17 @@ module Ruflet
167
197
  end
168
198
 
169
199
  def read_ext(reader, size)
170
- reader.read_i8
171
- reader.read_exact(size)
200
+ type = reader.read_i8
201
+ data = reader.read_exact(size)
202
+
203
+ case type
204
+ when 1, 2, 4
205
+ data.dup.force_encoding("UTF-8")
206
+ when 3
207
+ data.to_i
208
+ else
209
+ data
210
+ end
172
211
  end
173
212
  end
174
213
 
@@ -204,6 +243,7 @@ module Ruflet
204
243
  def read_f32 = read_exact(4).unpack1("g")
205
244
  def read_f64 = read_exact(8).unpack1("G")
206
245
  def read_string(size) = read_exact(size).force_encoding("UTF-8")
246
+ def read_binary(size) = read_exact(size)
207
247
  end
208
248
  end
209
249
  end
@@ -6,6 +6,25 @@ module Ruflet
6
6
  initializer "ruflet_rails.middleware" do |app|
7
7
  app.middleware.use Ruflet::Rails::Protocol::Middleware
8
8
  end
9
+
10
+ rake_tasks do
11
+ namespace :ruflet do
12
+ desc "Build Ruflet client for this Rails app. Usage: rake ruflet:build[web]"
13
+ task :build, [:platform] do |_task, args|
14
+ platform = args[:platform].to_s.strip
15
+ if platform.empty?
16
+ warn "Usage: rake ruflet:build[apk|android|ios|aab|web|macos|windows|linux]"
17
+ next
18
+ end
19
+
20
+ require "ruflet/cli"
21
+ exit_code = Dir.chdir(::Rails.root) do
22
+ Ruflet::CLI.command_build([platform])
23
+ end
24
+ raise SystemExit, exit_code unless exit_code.to_i.zero?
25
+ end
26
+ end
27
+ end
9
28
  end
10
29
  end
11
30
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruflet
4
- VERSION = "0.0.5" unless const_defined?(:VERSION)
4
+ VERSION = "0.0.7" unless const_defined?(:VERSION)
5
5
  end
data/lib/ruflet_rails.rb CHANGED
@@ -1,12 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- local_ruflet_lib = File.expand_path("../../ruflet/lib", __dir__)
4
- if File.directory?(local_ruflet_lib) && !$LOAD_PATH.include?(local_ruflet_lib)
5
- $LOAD_PATH.unshift(local_ruflet_lib)
3
+ %w[ruflet_core ruflet].reverse_each do |package|
4
+ local_package_lib = File.expand_path("../../#{package}/lib", __dir__)
5
+ if File.directory?(local_package_lib) && !$LOAD_PATH.include?(local_package_lib)
6
+ $LOAD_PATH.unshift(local_package_lib)
7
+ end
6
8
  end
7
9
 
8
- require "ruflet"
10
+ require "ruflet_core"
9
11
  require_relative "ruflet/rails/protocol"
12
+ require_relative "ruflet/rails/install_support"
10
13
  require_relative "ruflet/rails"
11
14
 
12
15
  if defined?(::Rails::Railtie)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruflet_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - AdamMusa
@@ -27,16 +27,30 @@ dependencies:
27
27
  name: ruflet
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
- - - '='
30
+ - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: 0.0.5
32
+ version: 0.0.13
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - '='
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 0.0.13
40
+ - !ruby/object:Gem::Dependency
41
+ name: ruflet_core
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 0.0.13
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
38
52
  - !ruby/object:Gem::Version
39
- version: 0.0.5
53
+ version: 0.0.13
40
54
  description: Rails-first integration package for mounting Ruflet mobile apps in Rails
41
55
  routes.
42
56
  email:
@@ -46,7 +60,9 @@ extensions: []
46
60
  extra_rdoc_files: []
47
61
  files:
48
62
  - README.md
63
+ - lib/generators/ruflet/install/install_generator.rb
49
64
  - lib/ruflet/rails.rb
65
+ - lib/ruflet/rails/install_support.rb
50
66
  - lib/ruflet/rails/protocol.rb
51
67
  - lib/ruflet/rails/protocol/context.rb
52
68
  - lib/ruflet/rails/protocol/endpoint.rb