-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f67f8fa
commit 4152c91
Showing
6 changed files
with
210 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
PATH | ||
remote: . | ||
specs: | ||
signalize (1.2.0) | ||
signalize (1.3.0) | ||
concurrent-ruby (~> 1.2) | ||
|
||
GEM | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
require "signalize" | ||
|
||
module Signalize | ||
class Struct | ||
module Accessors | ||
def members | ||
@members ||= [] | ||
end | ||
|
||
def signal_accessor(*names) | ||
names.each do |name| | ||
members.push(name.to_sym) unless members.find { _1 == name.to_sym } | ||
signal_getter_name = "#{name}_signal".freeze | ||
ivar_name = "@#{name}".freeze | ||
|
||
define_method "#{name}_signal" do | ||
instance_variable_get(ivar_name) | ||
end | ||
|
||
define_method name do | ||
send(signal_getter_name)&.value | ||
end | ||
|
||
define_method "#{name}=" do |val| | ||
if instance_variable_defined?(ivar_name) | ||
raise Signalize::Error, "Cannot assign a signal to a signal value" if val.is_a?(Signalize::Signal) | ||
|
||
sig = instance_variable_get(ivar_name) | ||
if sig.is_a?(Signalize::Computed) | ||
raise Signalize::Error, "Cannot set value of computed signal `#{ivar_name.delete_prefix("@")}'" | ||
end | ||
|
||
sig.value = val | ||
else | ||
val = Signalize.signal(val) unless val.is_a?(Signalize::Computed) | ||
instance_variable_set(ivar_name, val) | ||
end | ||
end | ||
end | ||
end | ||
end | ||
|
||
extend Accessors | ||
|
||
def self.define(*names, &block) | ||
Class.new(self).tap do |struct| | ||
struct.signal_accessor(*names) | ||
struct.class_eval(&block) if block | ||
end | ||
end | ||
|
||
def initialize(**data) | ||
# The below code is all to replicate native Ruby ergonomics | ||
unknown_keys = data.keys - members | ||
unless unknown_keys.empty? | ||
plural_suffix = unknown_keys.length > 1 ? "s" : "" | ||
raise ArgumentError, "unknown keyword#{plural_suffix}: #{unknown_keys.map { ":#{_1}" }.join(", ")}" | ||
end | ||
|
||
missing_keys = members - data.keys | ||
unless missing_keys.empty? | ||
plural_suffix = missing_keys.length > 1 ? "s" : "" | ||
raise ArgumentError, "missing keyword#{plural_suffix}: #{missing_keys.map { ":#{_1}" }.join(", ")}" | ||
end | ||
|
||
# Initialize with keyword arguments | ||
data.each do |k, v| | ||
send("#{k}=", v) | ||
end | ||
end | ||
|
||
def members = self.class.members | ||
|
||
def deconstruct_keys(...) = to_h.deconstruct_keys(...) | ||
|
||
def to_h = members.each_with_object({}) { _2[_1] = send("#{_1}_signal").peek } | ||
|
||
def inspect | ||
var_peeks = instance_variables.filter_map do |var_name| | ||
var = instance_variable_get(var_name) | ||
"#{var_name.to_s.delete_prefix("@")}=#{var.peek.inspect}" if var.is_a?(Signalize::Signal) | ||
end.join(", ") | ||
|
||
"#<#{self.class}#{var_peeks.empty? ? nil : " #{var_peeks}"}>" | ||
end | ||
|
||
def to_s | ||
inspect | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
# frozen_string_literal: true | ||
|
||
module Signalize | ||
VERSION = "1.2.0" | ||
VERSION = "1.3.0" | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# frozen_string_literal: true | ||
|
||
require "test_helper" | ||
require "signalize/struct" | ||
|
||
class TestStruct < Minitest::Test | ||
include Signalize::API | ||
|
||
TestSignalsStruct = Signalize::Struct.define( | ||
:str, | ||
:int | ||
) do | ||
def increment! | ||
self.int += 1 | ||
end | ||
end | ||
|
||
def test_int_value | ||
struct = TestSignalsStruct.new(int: 0, str: "") | ||
|
||
assert_equal 0, struct.int | ||
assert_equal 0, struct.int_signal.value | ||
|
||
# Write to a signal | ||
struct.int = 1 | ||
|
||
assert_equal 1, struct.int | ||
|
||
struct.increment! | ||
|
||
assert_equal 2, struct.int | ||
end | ||
|
||
def test_str_computed | ||
struct = TestSignalsStruct.new(str: "Doe", int: 0) | ||
name = signal("Jane") | ||
|
||
computed_ran_once = 0 | ||
|
||
full_name = computed do | ||
computed_ran_once += 1 | ||
name.value + " " + struct.str | ||
end | ||
|
||
assert_equal "Jane Doe", full_name.value | ||
|
||
name.value = "John" | ||
name.value = "Johannes" | ||
# name.value = "..." | ||
# Setting value multiple times won't trigger a computed value refresh | ||
|
||
# NOW we get a refreshed computed value: | ||
assert_equal "Johannes Doe", full_name.value | ||
assert_equal 2, computed_ran_once | ||
|
||
# Test deconstructing | ||
struct => { str: } | ||
assert_equal "Doe", str | ||
end | ||
end |