lowdata 0.3.0 → 0.4.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.
- checksums.yaml +4 -4
- data/lib/lowdata.rb +25 -0
- data/lib/nesting_path.rb +26 -1
- data/lib/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5b4ee6fd29a6d15edeb3d85e8517c7b7a3a8d2256e8839479bf5596087879da9
|
|
4
|
+
data.tar.gz: a383a5cd9e4153e59ebe872af635bd7b96880a0fb38e347f8e6a6094d0163e73
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '009095ae108f1118421d6ca8945b66e93664ed1b1b45f8fb120c9204d3a359f4b16d789a60f0001a07cd058b788c25484929f020fc6dba19e36e432cbee7eb4d'
|
|
7
|
+
data.tar.gz: 6431685585fe4c8cbd8703ebe7c17c50dd22292f4055912c65ba0673e7af424b8be54f4a41c444190f432aa90b248b939ea83f53827961367b354b81932037eb
|
data/lib/lowdata.rb
CHANGED
|
@@ -44,6 +44,23 @@ class LowData
|
|
|
44
44
|
@table ||= name && table_name(name.split('::').last.delete_suffix('Data'))
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
# The Method Factory pattern, as in LowNode: each public instance method gets a class method of the same name, so
|
|
48
|
+
# `UsersData.update(1, attributes)` calls `UsersData.new.update(1, attributes)`. Defined when the instance method
|
|
49
|
+
# is, so it's a real method rather than a method_missing lookup. Existing class methods, like `table`, are kept.
|
|
50
|
+
def method_added(name)
|
|
51
|
+
super
|
|
52
|
+
return if self == LowData || !public_method_defined?(name)
|
|
53
|
+
return unless name.match?(/\A[a-z_]\w*[?!]?\z/) # Setters like `title=` stay instance methods.
|
|
54
|
+
return if factory_method?(name) # A parent's class method already calls `new` on this class.
|
|
55
|
+
return warn(class_method_exists_message(name:)) if respond_to?(name, true)
|
|
56
|
+
|
|
57
|
+
(@factory_methods ||= []) << name
|
|
58
|
+
# public_send because `private def secret` makes the method private only after it's added.
|
|
59
|
+
singleton_class.class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
60
|
+
def #{name}(...) = new.public_send(:#{name}, ...) # def update(...) = new.public_send(:update, ...)
|
|
61
|
+
RUBY
|
|
62
|
+
end
|
|
63
|
+
|
|
47
64
|
# Resolve undefined constants in a repository to tables, so `Users` and `BlogPosts` become :users and :blog_posts.
|
|
48
65
|
# `Table` is the repository's own table, joined to whichever related tables own the columns asked for.
|
|
49
66
|
def const_missing(name)
|
|
@@ -64,6 +81,14 @@ class LowData
|
|
|
64
81
|
|
|
65
82
|
private
|
|
66
83
|
|
|
84
|
+
def factory_method?(name)
|
|
85
|
+
self < LowData && (@factory_methods&.include?(name) || superclass.send(:factory_method?, name))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def class_method_exists_message(name:)
|
|
89
|
+
"#{self}##{name} has no class method, because #{self}.#{name} exists. Call #{self}.new.#{name}"
|
|
90
|
+
end
|
|
91
|
+
|
|
67
92
|
def inverted_table
|
|
68
93
|
raise ConfigError, "Set the table for #{self}, e.g. self.table = :posts" if table.nil?
|
|
69
94
|
|
data/lib/nesting_path.rb
CHANGED
|
@@ -10,6 +10,8 @@ class LowData
|
|
|
10
10
|
class NestingPath
|
|
11
11
|
ARRAY_END_MESSAGE = 'An array ends a + chain, because it nests several tables. Put the rest of the chain in the ' \
|
|
12
12
|
'array, e.g. Users + [Posts + Comments, Orders]'
|
|
13
|
+
AFTER_ARRAY_MESSAGE = '%<child>s comes after an array, so it has no single table to nest in. Put it in the array: ' \
|
|
14
|
+
'%<beside>s nests %<child>s in %<parent>s, and %<in_last>s nests it in %<last>s'
|
|
13
15
|
|
|
14
16
|
# A node waiting for its children to be linked, and the entries from it down to the end of its chain.
|
|
15
17
|
Entry = Struct.new(:node, :children, :tail, :closed) do
|
|
@@ -43,16 +45,19 @@ class LowData
|
|
|
43
45
|
|
|
44
46
|
# Nests a segment and returns the path to the end of the chain.
|
|
45
47
|
def attach(path:, segment:)
|
|
46
|
-
raise JoinError, ARRAY_END_MESSAGE if path.last.closed
|
|
47
48
|
return siblings(path:, nodes: segment.nodes) if segment.is_a?(DataExpression::Siblings)
|
|
48
49
|
|
|
49
50
|
child = @tree.entry(node: segment)
|
|
51
|
+
raise JoinError, after_array_message(array: path.last, child:) if path.last.closed
|
|
52
|
+
|
|
50
53
|
parent = parent(path:, child:)
|
|
51
54
|
parent.children << child
|
|
52
55
|
path[0..path.index { |entry| entry.equal?(parent) }] + child.path
|
|
53
56
|
end
|
|
54
57
|
|
|
55
58
|
def siblings(path:, nodes:)
|
|
59
|
+
raise JoinError, ARRAY_END_MESSAGE if path.last.closed
|
|
60
|
+
|
|
56
61
|
nodes.each { |node| path.last.children << @tree.entry(node:) }
|
|
57
62
|
path.last.closed = true
|
|
58
63
|
path
|
|
@@ -94,6 +99,26 @@ class LowData
|
|
|
94
99
|
entry.tables.product(other.tables).any? { |table, other_table| @schema.linked?(table, other_table) }
|
|
95
100
|
end
|
|
96
101
|
|
|
102
|
+
# Suggests the two places in the array a table after it can go: beside its elements, or in its last one.
|
|
103
|
+
def after_array_message(array:, child:)
|
|
104
|
+
*elements, last = array.children
|
|
105
|
+
beside = [*elements, last, child].map { |entry| chained(entry:) }
|
|
106
|
+
in_last = [*elements.map { |entry| chained(entry:) }, "#{chained(entry: last)} + #{chained(entry: child)}"]
|
|
107
|
+
format(AFTER_ARRAY_MESSAGE, child: child.tables.first, parent: array.tables.last, last: last.path.last.tables.last,
|
|
108
|
+
beside: in_array(array:, elements: beside), in_last: in_array(array:, elements: in_last))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def in_array(array:, elements:) = "#{written(entry: array)} + [#{elements.join(', ')}]"
|
|
112
|
+
|
|
113
|
+
# An array element as written, with its whole chain: `Posts + Comments`.
|
|
114
|
+
def chained(entry:) = entry.path.map { |link| written(entry: link) }.join(' + ')
|
|
115
|
+
|
|
116
|
+
# A table as written in an expression, without its brackets: `Posts`, or `(Posts & Likes)` for a join.
|
|
117
|
+
def written(entry:)
|
|
118
|
+
names = entry.tables.map { |table| LowData.constant_name(table) }
|
|
119
|
+
names.one? ? names.first : "(#{names.join(' & ')})"
|
|
120
|
+
end
|
|
121
|
+
|
|
97
122
|
# Suggests an array, or naming the foreign key that reaches back to the earlier table.
|
|
98
123
|
def unlinked_message(child:, parent:, earlier:)
|
|
99
124
|
message = "No foreign key links #{child} to #{parent}"
|
data/lib/version.rb
CHANGED