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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 00edcbe16f363ff5571130711b163c2b7a30f269b063237d9513f0d0d65b1ac9
4
- data.tar.gz: b76da81116417a88cb6d440acfcd676aee2f9f53726c8b079551e4ab6df5279f
3
+ metadata.gz: 5b4ee6fd29a6d15edeb3d85e8517c7b7a3a8d2256e8839479bf5596087879da9
4
+ data.tar.gz: a383a5cd9e4153e59ebe872af635bd7b96880a0fb38e347f8e6a6094d0163e73
5
5
  SHA512:
6
- metadata.gz: 76724feffd8a520b11e60bd387ebf1a3aa7543fc7bb962af8b4b5db4411f9099fa42cef7207c98592cf6687a393076b51f3ffad45b198e32b885f8636accac93
7
- data.tar.gz: f555c5e21c68df568a8e02e511f9fd6ee8ced5ca9a97edc799eb95415e96fb56c38b2711eb161546fe7c00b775905c1418127b146caddb474a96c9ede039062b
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class LowData
4
- VERSION = '0.3.0'
4
+ VERSION = '0.4.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lowdata
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi