yaml_exporter 0.2.0 → 0.2.1
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/CHANGELOG.md +11 -0
- data/Gemfile.lock +32 -32
- data/README.md +16 -3
- data/lib/yaml_exporter/builder.rb +13 -3
- data/lib/yaml_exporter/nodes/many_reference.rb +33 -7
- data/lib/yaml_exporter/nodes/many_through.rb +27 -8
- data/lib/yaml_exporter/nodes/of_resolution.rb +84 -0
- data/lib/yaml_exporter/nodes/one_reference_of.rb +10 -53
- data/lib/yaml_exporter/version.rb +1 -1
- data/lib/yaml_exporter.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6c15fc079f4444a43a53ea849f8ff86162074ace88cc884ed28bef127a59afc0
|
|
4
|
+
data.tar.gz: 6b51f04b4712e71f1840c61fb323be7127b133e561afaaafcd81f9e382f7919b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4beae00561aeb8d7da659b6b3b578cb9e9aa95bfcd2d266a63bc9fb0e98a6a6f1310cd73801edc03125fb3c831b6a730458165b56549067a0b5a5eb276a114a8
|
|
7
|
+
data.tar.gz: 19dfc281188c17041c1d822422b1eb07f11aea06525ba042b261f016945e664ae40ac272e10246f280fbe5c70d1b0f3633a809ec4cb9ea4363eb6563ddf2dc8e
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ This project follows [semantic versioning](https://semver.org).
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## v0.2.1
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `of:` on `many` reference lists. Both `many :assoc, find_by: :col, of: :nested`
|
|
13
|
+
and the `through:` flavor `many :assoc, through: :join, find_by: :col, of: :nested`
|
|
14
|
+
now identify each target indirectly via a 1:[0,1] association on the target
|
|
15
|
+
(`of:`), mirroring the existing `one … find_by: of:` semantics. The YAML stays
|
|
16
|
+
a flat list of the related model's `find_by` values (e.g. user slugs) instead
|
|
17
|
+
of requiring a nested block of hashes.
|
|
18
|
+
|
|
8
19
|
## v0.2.0
|
|
9
20
|
|
|
10
21
|
A ground-up rewrite of the export/import engine and its DSL. **Breaking: nothing
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
yaml_exporter (0.2.
|
|
4
|
+
yaml_exporter (0.2.1)
|
|
5
5
|
activerecord (>= 5.2)
|
|
6
6
|
activesupport (>= 5.2)
|
|
7
7
|
|
|
@@ -28,27 +28,27 @@ GEM
|
|
|
28
28
|
tzinfo (~> 2.0, >= 2.0.5)
|
|
29
29
|
uri (>= 0.13.1)
|
|
30
30
|
base64 (0.3.0)
|
|
31
|
-
bigdecimal (4.1.
|
|
32
|
-
concurrent-ruby (1.3.
|
|
31
|
+
bigdecimal (4.1.2)
|
|
32
|
+
concurrent-ruby (1.3.7)
|
|
33
33
|
connection_pool (3.0.2)
|
|
34
34
|
drb (2.2.3)
|
|
35
|
-
i18n (1.
|
|
35
|
+
i18n (1.15.2)
|
|
36
36
|
concurrent-ruby (~> 1.0)
|
|
37
|
-
json (2.
|
|
37
|
+
json (2.21.1)
|
|
38
38
|
logger (1.7.0)
|
|
39
39
|
minitest (5.27.0)
|
|
40
|
-
rake (13.
|
|
40
|
+
rake (13.4.2)
|
|
41
41
|
securerandom (0.4.1)
|
|
42
|
-
sqlite3 (2.9.
|
|
43
|
-
sqlite3 (2.9.
|
|
44
|
-
sqlite3 (2.9.
|
|
45
|
-
sqlite3 (2.9.
|
|
46
|
-
sqlite3 (2.9.
|
|
47
|
-
sqlite3 (2.9.
|
|
48
|
-
sqlite3 (2.9.
|
|
49
|
-
sqlite3 (2.9.
|
|
50
|
-
sqlite3 (2.9.
|
|
51
|
-
sqlite3 (2.9.
|
|
42
|
+
sqlite3 (2.9.5-aarch64-linux-gnu)
|
|
43
|
+
sqlite3 (2.9.5-aarch64-linux-musl)
|
|
44
|
+
sqlite3 (2.9.5-arm-linux-gnu)
|
|
45
|
+
sqlite3 (2.9.5-arm-linux-musl)
|
|
46
|
+
sqlite3 (2.9.5-arm64-darwin)
|
|
47
|
+
sqlite3 (2.9.5-x86-linux-gnu)
|
|
48
|
+
sqlite3 (2.9.5-x86-linux-musl)
|
|
49
|
+
sqlite3 (2.9.5-x86_64-darwin)
|
|
50
|
+
sqlite3 (2.9.5-x86_64-linux-gnu)
|
|
51
|
+
sqlite3 (2.9.5-x86_64-linux-musl)
|
|
52
52
|
timeout (0.6.1)
|
|
53
53
|
tzinfo (2.0.6)
|
|
54
54
|
concurrent-ruby (~> 1.0)
|
|
@@ -77,30 +77,30 @@ CHECKSUMS
|
|
|
77
77
|
activerecord (8.1.3) sha256=8003be7b2466ba0a2a670e603eeb0a61dd66058fccecfc49901e775260ac70ab
|
|
78
78
|
activesupport (8.1.3) sha256=21a5e0dfbd4c3ddd9e1317ec6a4d782fa226e7867dc70b0743acda81a1dca20e
|
|
79
79
|
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
|
80
|
-
bigdecimal (4.1.
|
|
81
|
-
concurrent-ruby (1.3.
|
|
80
|
+
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
|
|
81
|
+
concurrent-ruby (1.3.7) sha256=4412caec3a5ea2e5fdc52076724c071a81f2c0593d83b2ac8cbb8ca63b3151b0
|
|
82
82
|
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
|
|
83
83
|
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
|
|
84
|
-
i18n (1.
|
|
85
|
-
json (2.
|
|
84
|
+
i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5
|
|
85
|
+
json (2.21.1) sha256=13a43df75d95641443f5702dff350f237164a9d811ff0f2c2800d4d980220583
|
|
86
86
|
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
|
87
87
|
minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5
|
|
88
|
-
rake (13.
|
|
88
|
+
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
|
|
89
89
|
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
|
|
90
|
-
sqlite3 (2.9.
|
|
91
|
-
sqlite3 (2.9.
|
|
92
|
-
sqlite3 (2.9.
|
|
93
|
-
sqlite3 (2.9.
|
|
94
|
-
sqlite3 (2.9.
|
|
95
|
-
sqlite3 (2.9.
|
|
96
|
-
sqlite3 (2.9.
|
|
97
|
-
sqlite3 (2.9.
|
|
98
|
-
sqlite3 (2.9.
|
|
99
|
-
sqlite3 (2.9.
|
|
90
|
+
sqlite3 (2.9.5-aarch64-linux-gnu) sha256=78075b6337d3d182c6d2b4691049ed45cd220826160c9ea18946bf6a1de200dc
|
|
91
|
+
sqlite3 (2.9.5-aarch64-linux-musl) sha256=18c801185deb4adc01ddb281e8f672a39e3d1729979ca91e39439cd3eac0402d
|
|
92
|
+
sqlite3 (2.9.5-arm-linux-gnu) sha256=1bdfca0c7d63998c60b0f4a8e3c8df2d33800ccc4abd2d612eddbbbc92a4c48b
|
|
93
|
+
sqlite3 (2.9.5-arm-linux-musl) sha256=bae1109d12b2e9f588455967729b008e1ff4feb7761749df695019c9079913c6
|
|
94
|
+
sqlite3 (2.9.5-arm64-darwin) sha256=d0cf444a70fc9395d513cfbcc1e6719e224aa645314e3824cb0474c721425aa2
|
|
95
|
+
sqlite3 (2.9.5-x86-linux-gnu) sha256=c94b96b16f17796be6fa099d15218b52e396f55690c4760faaaefa21ebab9dd5
|
|
96
|
+
sqlite3 (2.9.5-x86-linux-musl) sha256=063a8c13cbadfe7f29453b1706cbdf91fca4a78d244f816ff20bac4fb259f1e4
|
|
97
|
+
sqlite3 (2.9.5-x86_64-darwin) sha256=8e9caae38bd7ebb29cbeee3e7ab1d12dc2327d9a1b92c7fcf0dda05589627a81
|
|
98
|
+
sqlite3 (2.9.5-x86_64-linux-gnu) sha256=233dbcb6714148dd23bc5aeb33e8efd6eac974969564ddd5794c23d5f52b231e
|
|
99
|
+
sqlite3 (2.9.5-x86_64-linux-musl) sha256=e7d3a7474e8af0f96150c21abc203fbab5437206bfcdf11deab7741c0ca516f2
|
|
100
100
|
timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb
|
|
101
101
|
tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b
|
|
102
102
|
uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6
|
|
103
|
-
yaml_exporter (0.2.
|
|
103
|
+
yaml_exporter (0.2.1)
|
|
104
104
|
|
|
105
105
|
BUNDLED WITH
|
|
106
106
|
4.0.6
|
data/README.md
CHANGED
|
@@ -112,7 +112,9 @@ The arguments you pass determine both the YAML shape and who owns the record:
|
|
|
112
112
|
| `many :book_parts, find_by: :slug do … end` | list of hashes, matched by `slug` |
|
|
113
113
|
| `many :book_parts, find_by: :slug, positioned_by: :position do … end` | list of hashes, matched by `slug`; no `position:` key — the column is derived from the array index |
|
|
114
114
|
| `many :authors, find_by: :slug` | list of bare strings |
|
|
115
|
+
| `many :editorial_editors, find_by: :slug, of: :user` | list of bare strings, each resolved via a nested association on the target |
|
|
115
116
|
| `many :reviewers, through: :book_reviewers, find_by: :slug` | list of bare strings (join rows managed for you; add `positioned_by:` to derive a join column from order) |
|
|
117
|
+
| `many :editorial_editors, through: :editor_assignments, find_by: :slug, of: :user` | list of bare strings, each resolved via a nested association on the target |
|
|
116
118
|
| `many :reviewers, through: :book_reviewers, find_by: :slug do … end` | list of hashes describing join-model attributes |
|
|
117
119
|
|
|
118
120
|
### Lifecycle rules
|
|
@@ -349,6 +351,13 @@ This same flavor works for `has_and_belongs_to_many` (as above) and for any `has
|
|
|
349
351
|
* If the database has more associations than the yaml file, the extra associations are removed — for HABTM only the join rows are removed, the referenced records themselves are left untouched.
|
|
350
352
|
* The order of the entries in the yaml file doesn't matter.
|
|
351
353
|
|
|
354
|
+
`of:` works on reference lists too, exactly as it does for [`one`](#one-with-find_by-and-of-indirect-reference) — the YAML value is a column on a companion record (`of:`) instead of on the target. It applies to both the plain and the `through:` reference list:
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
many :corporate_reviewers, find_by: :slug, of: :user
|
|
358
|
+
many :editorial_editors, through: :editor_assignments, find_by: :slug, of: :user
|
|
359
|
+
```
|
|
360
|
+
|
|
352
361
|
### `many` with `through:` (has_many :through with join attributes)
|
|
353
362
|
|
|
354
363
|
`has_many :through` is not a separate DSL method — how you expose it depends on whether the join model carries its own attributes.
|
|
@@ -793,9 +802,9 @@ A single related record. Exactly one of `find_by:` or a block must be given:
|
|
|
793
802
|
|
|
794
803
|
Passing both a block and `find_by:` is rejected — see the ownership reasoning in [`one` with `find_by`](#one-with-find_by-reference).
|
|
795
804
|
|
|
796
|
-
### `many(name, positioned_by: nil, find_by: nil, through: nil, &block)`
|
|
805
|
+
### `many(name, positioned_by: nil, find_by: nil, through: nil, of: nil, &block)`
|
|
797
806
|
|
|
798
|
-
A list of related records. The flavor is picked from the combination of `positioned_by:`, `find_by:`, `through:` and a block:
|
|
807
|
+
A list of related records. The flavor is picked from the combination of `positioned_by:`, `find_by:`, `through:`, `of:` and a block:
|
|
799
808
|
|
|
800
809
|
| Call | YAML shape | Meaning |
|
|
801
810
|
| -------------------------------------------------------------------------------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
@@ -804,10 +813,14 @@ A list of related records. The flavor is picked from the combination of `positio
|
|
|
804
813
|
| `many :children, find_by: :slug do … end` | list of hashes containing the `slug:` key | Children fully managed, identity by a stable column. |
|
|
805
814
|
| `many :children, find_by: :slug, positioned_by: :position do … end` | same as above, without `position:` in each hash | Like the previous row, but the named column is derived from the 1-based array index (and omitted on export). |
|
|
806
815
|
| `many :children, find_by: :slug` | list of bare strings | Children referenced by key, managed elsewhere (HABTM pattern). |
|
|
816
|
+
| `many :children, find_by: :slug, of: :companion` | list of bare strings | Reference list keyed indirectly: each value lives on a `companion` (1:[0,1]) association of the target. |
|
|
807
817
|
| `many :children, through: :joins, find_by: :slug do … end` | list of hashes containing the `slug:` key | `has_many :through` where the block describes attributes of the **join model**. |
|
|
808
818
|
| `many :children, through: :joins, find_by: :slug, positioned_by: :position do …` | same as above, without `position:` in each hash | As above, with the position column derived on the **join model**. |
|
|
819
|
+
| `many :children, through: :joins, find_by: :slug, of: :companion` | list of bare strings | Join-model reference list keyed indirectly via the target's `companion` association (`positioned_by:` optional).|
|
|
820
|
+
|
|
821
|
+
`positioned_by:` requires a block – there must be an owned record to write the column onto – and therefore cannot be used with the plain reference-list flavor of `many` (it *is* allowed on the `through:` reference list, where it drives the join's position column).
|
|
809
822
|
|
|
810
|
-
`
|
|
823
|
+
`of:` requires `find_by:` and cannot be combined with a block, and the `of:` association must be a 1:[0,1] relation (`belongs_to` or `has_one`) — same rules and semantics as [`one … of:`](#one-with-find_by-and-of-indirect-reference).
|
|
811
824
|
|
|
812
825
|
### Import / export
|
|
813
826
|
|
|
@@ -49,7 +49,17 @@ module YamlExporter
|
|
|
49
49
|
end
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
def many(name, find_by: nil, through: nil, positioned_by: nil, &block)
|
|
52
|
+
def many(name, find_by: nil, through: nil, positioned_by: nil, of: nil, &block)
|
|
53
|
+
if of && !find_by
|
|
54
|
+
raise ArgumentError,
|
|
55
|
+
"`many #{name.inspect}`: of: requires find_by:."
|
|
56
|
+
end
|
|
57
|
+
if of && block
|
|
58
|
+
raise ArgumentError,
|
|
59
|
+
"`many #{name.inspect}`: of: cannot combine with a block — of: only applies to " \
|
|
60
|
+
'reference lists (no block).'
|
|
61
|
+
end
|
|
62
|
+
|
|
53
63
|
if through
|
|
54
64
|
unless find_by
|
|
55
65
|
raise ArgumentError,
|
|
@@ -61,7 +71,7 @@ module YamlExporter
|
|
|
61
71
|
# the YAML order). The join row itself is always managed by the DSL.
|
|
62
72
|
@nodes << Nodes::ManyThrough.new(
|
|
63
73
|
name: name, owner_class: klass, through: through, find_by: find_by,
|
|
64
|
-
positioned_by: positioned_by, &block
|
|
74
|
+
positioned_by: positioned_by, of: of, &block
|
|
65
75
|
)
|
|
66
76
|
elsif positioned_by && !block
|
|
67
77
|
raise ArgumentError,
|
|
@@ -76,7 +86,7 @@ module YamlExporter
|
|
|
76
86
|
name: name, owner_class: klass, positioned_by: positioned_by, &block
|
|
77
87
|
)
|
|
78
88
|
elsif find_by
|
|
79
|
-
@nodes << Nodes::ManyReference.new(name: name, owner_class: klass, find_by: find_by)
|
|
89
|
+
@nodes << Nodes::ManyReference.new(name: name, owner_class: klass, find_by: find_by, of: of)
|
|
80
90
|
else
|
|
81
91
|
raise ArgumentError,
|
|
82
92
|
"`many #{name.inspect}`: pass either find_by: (reference list), a block (owned), " \
|
|
@@ -7,19 +7,29 @@ module YamlExporter
|
|
|
7
7
|
# an existing target record via `find_by`. Target records are never
|
|
8
8
|
# created or mutated; only the association is updated.
|
|
9
9
|
#
|
|
10
|
+
# With `of:` the value is resolved indirectly through a 1:[0,1]
|
|
11
|
+
# association on the target (see OfResolution), so the YAML lists the
|
|
12
|
+
# find_by value of a *related* model (e.g. the user slug behind each
|
|
13
|
+
# target) rather than a column on the target itself.
|
|
14
|
+
#
|
|
10
15
|
# Phase: :post_save (join rows / FK need parent.id).
|
|
11
16
|
class ManyReference
|
|
12
|
-
|
|
17
|
+
include OfResolution
|
|
18
|
+
|
|
19
|
+
attr_reader :name, :owner_class, :find_by, :of
|
|
13
20
|
|
|
14
|
-
def initialize(name:, owner_class:, find_by:)
|
|
21
|
+
def initialize(name:, owner_class:, find_by:, of: nil)
|
|
15
22
|
@name = name.to_sym
|
|
16
23
|
@owner_class = owner_class
|
|
17
24
|
@find_by = find_by.to_sym
|
|
25
|
+
@of = of&.to_sym
|
|
18
26
|
reflection = owner_class.reflect_on_association(@name)
|
|
19
27
|
unless reflection
|
|
20
28
|
raise ArgumentError,
|
|
21
29
|
"`many #{name.inspect}`: #{owner_class} has no association `#{name}`"
|
|
22
30
|
end
|
|
31
|
+
|
|
32
|
+
validate_of_reflection!("many #{name.inspect}") if @of
|
|
23
33
|
end
|
|
24
34
|
|
|
25
35
|
def phase
|
|
@@ -44,9 +54,8 @@ module YamlExporter
|
|
|
44
54
|
end
|
|
45
55
|
|
|
46
56
|
targets = values.map do |value|
|
|
47
|
-
|
|
48
|
-
(raise ActiveRecord::RecordNotFound,
|
|
49
|
-
"no #{target_class} with #{@find_by}=#{value.inspect}")
|
|
57
|
+
resolve_target(value) ||
|
|
58
|
+
(raise ActiveRecord::RecordNotFound, not_found_message(value))
|
|
50
59
|
end
|
|
51
60
|
|
|
52
61
|
record.public_send("#{@name}=", targets)
|
|
@@ -54,17 +63,34 @@ module YamlExporter
|
|
|
54
63
|
|
|
55
64
|
def export(record, exporter:)
|
|
56
65
|
targets = Array(record.public_send(@name))
|
|
57
|
-
keys = targets.map { |t| t
|
|
66
|
+
keys = targets.map { |t| key_for(t) }.compact.sort
|
|
58
67
|
[@name.to_s, keys]
|
|
59
68
|
end
|
|
60
69
|
|
|
61
70
|
def schema_fragment
|
|
62
|
-
|
|
71
|
+
key_class = @of ? of_class : target_class
|
|
72
|
+
item_type = TypeInference.schema_type_for(key_class, @find_by)
|
|
63
73
|
{ @name => { type: 'array', items: { type: item_type } } }
|
|
64
74
|
end
|
|
65
75
|
|
|
66
76
|
private
|
|
67
77
|
|
|
78
|
+
def resolve_target(value)
|
|
79
|
+
@of ? resolve_target_by_of(value) : target_class.find_by(@find_by => value)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def key_for(target)
|
|
83
|
+
@of ? of_value_for(target) : target.public_send(@find_by)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def not_found_message(value)
|
|
87
|
+
if @of
|
|
88
|
+
"no #{target_class} for #{of_class} with #{@find_by}=#{value.inspect} via `#{@of}`"
|
|
89
|
+
else
|
|
90
|
+
"no #{target_class} with #{@find_by}=#{value.inspect}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
68
94
|
def describe_path(path)
|
|
69
95
|
path.empty? ? @name.to_s : "#{path}.#{@name}"
|
|
70
96
|
end
|
|
@@ -16,15 +16,18 @@ module YamlExporter
|
|
|
16
16
|
# the reference flavor: passing a block (empty or not) opts into the
|
|
17
17
|
# hash-shaped entries, same as the other `many` flavors.
|
|
18
18
|
class ManyThrough < ManyBase
|
|
19
|
-
|
|
19
|
+
include OfResolution
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
attr_reader :through, :of
|
|
22
|
+
|
|
23
|
+
def initialize(name:, owner_class:, through:, find_by:, positioned_by: nil, of: nil, &block)
|
|
22
24
|
# Whether a block was passed (even an empty one) decides the YAML
|
|
23
25
|
# shape: block → hash entries, no block → bare reference list.
|
|
24
26
|
@reference_list = block.nil?
|
|
25
27
|
block ||= proc {}
|
|
26
28
|
|
|
27
29
|
@through = through.to_sym
|
|
30
|
+
@of = of&.to_sym
|
|
28
31
|
join_reflection = owner_class.reflect_on_association(@through)
|
|
29
32
|
unless join_reflection
|
|
30
33
|
raise ArgumentError,
|
|
@@ -33,6 +36,8 @@ module YamlExporter
|
|
|
33
36
|
|
|
34
37
|
super(name: name, owner_class: owner_class, find_by: find_by,
|
|
35
38
|
positioned_by: positioned_by, &block)
|
|
39
|
+
|
|
40
|
+
validate_of_reflection!("many #{name.inspect}") if @of
|
|
36
41
|
end
|
|
37
42
|
|
|
38
43
|
# No block was passed → the YAML is a flat list of find_by values
|
|
@@ -63,10 +68,9 @@ module YamlExporter
|
|
|
63
68
|
|
|
64
69
|
def find_or_build_child(parent, entry, _index, existing:)
|
|
65
70
|
key = entry[@find_by.to_s]
|
|
66
|
-
target = target_class.find_by(@find_by => key)
|
|
71
|
+
target = @of ? resolve_target_by_of(key) : target_class.find_by(@find_by => key)
|
|
67
72
|
unless target
|
|
68
|
-
raise ActiveRecord::RecordNotFound,
|
|
69
|
-
"no #{target_class} with #{@find_by}=#{key.inspect}"
|
|
73
|
+
raise ActiveRecord::RecordNotFound, not_found_message(key)
|
|
70
74
|
end
|
|
71
75
|
|
|
72
76
|
# Match existing join rows by comparing the source association
|
|
@@ -87,7 +91,7 @@ module YamlExporter
|
|
|
87
91
|
def default_export_order(records)
|
|
88
92
|
records.sort_by do |join|
|
|
89
93
|
target = join.public_send(source_association_name)
|
|
90
|
-
target
|
|
94
|
+
key_for(target).to_s
|
|
91
95
|
end
|
|
92
96
|
end
|
|
93
97
|
|
|
@@ -96,7 +100,7 @@ module YamlExporter
|
|
|
96
100
|
def export(parent, exporter:)
|
|
97
101
|
records = sort_for_export(Array(parent.public_send(@through)))
|
|
98
102
|
if reference_list?
|
|
99
|
-
keys = records.map { |join| join.public_send(source_association_name)
|
|
103
|
+
keys = records.map { |join| key_for(join.public_send(source_association_name)) }
|
|
100
104
|
return [@name.to_s, keys]
|
|
101
105
|
end
|
|
102
106
|
|
|
@@ -158,12 +162,27 @@ module YamlExporter
|
|
|
158
162
|
def schema_fragment
|
|
159
163
|
return super unless reference_list?
|
|
160
164
|
|
|
161
|
-
|
|
165
|
+
key_class = @of ? of_class : target_class
|
|
166
|
+
item_type = TypeInference.schema_type_for(key_class, @find_by)
|
|
162
167
|
{ @name => { type: 'array', items: { type: item_type } } }
|
|
163
168
|
end
|
|
164
169
|
|
|
165
170
|
private
|
|
166
171
|
|
|
172
|
+
# The YAML value for a resolved target: with `of:` it lives on a related
|
|
173
|
+
# model; otherwise it's the find_by column on the target itself.
|
|
174
|
+
def key_for(target)
|
|
175
|
+
@of ? of_value_for(target) : target.public_send(@find_by)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def not_found_message(key)
|
|
179
|
+
if @of
|
|
180
|
+
"no #{target_class} for #{of_class} with #{@find_by}=#{key.inspect} via `#{@of}`"
|
|
181
|
+
else
|
|
182
|
+
"no #{target_class} with #{@find_by}=#{key.inspect}"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
167
186
|
def source_reflection
|
|
168
187
|
@source_reflection ||= @owner_class.reflect_on_association(@name).source_reflection
|
|
169
188
|
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YamlExporter
|
|
4
|
+
module Nodes
|
|
5
|
+
# Shared `of:` navigation for reference nodes that identify their target
|
|
6
|
+
# indirectly: the YAML value names a column on a *related* model
|
|
7
|
+
# (`of:`) reachable from the target, not on the target itself.
|
|
8
|
+
#
|
|
9
|
+
# Example: a reviewer target is a CorporateUser, and
|
|
10
|
+
# CorporateUser `belongs_to :user` (a User with a slug). Declaring
|
|
11
|
+
# `find_by: :slug, of: :user` stores the User's slug in the YAML and, on
|
|
12
|
+
# import, resolves the User first and then reverses `of:` back to the
|
|
13
|
+
# CorporateUser.
|
|
14
|
+
#
|
|
15
|
+
# Mixed into OneReferenceOf, ManyReference and ManyThrough. The host must
|
|
16
|
+
# provide `target_class` and set `@find_by` and `@of` (both symbols).
|
|
17
|
+
module OfResolution
|
|
18
|
+
# The related model that actually carries the find_by column
|
|
19
|
+
# (CorporateUser.user -> User).
|
|
20
|
+
def of_reflection
|
|
21
|
+
@of_reflection ||= target_class.reflect_on_association(@of)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The class of the related model (CorporateUser.user -> User).
|
|
25
|
+
def of_class
|
|
26
|
+
@of_class ||= of_reflection.klass
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Resolve a single YAML value to a target record: find the related
|
|
30
|
+
# record by find_by, then reverse `of:` back to the target. Returns nil
|
|
31
|
+
# if either lookup misses.
|
|
32
|
+
def resolve_target_by_of(value)
|
|
33
|
+
related = of_class.find_by(@find_by => value)
|
|
34
|
+
return nil unless related
|
|
35
|
+
|
|
36
|
+
find_target_via(related)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Navigate from a resolved related record back to the target.
|
|
40
|
+
#
|
|
41
|
+
# belongs_to (:user on CorporateUser): FK is on the target.
|
|
42
|
+
# target_class.find_by(user_id: related.id)
|
|
43
|
+
# has_one (:profile on CorporateUser): FK is on the related record.
|
|
44
|
+
# target_class.find_by(id: related.corporate_user_id)
|
|
45
|
+
def find_target_via(related)
|
|
46
|
+
if of_reflection.is_a?(ActiveRecord::Reflection::BelongsToReflection)
|
|
47
|
+
pk_val = related.public_send(of_reflection.association_primary_key)
|
|
48
|
+
target_class.find_by(of_reflection.foreign_key => pk_val)
|
|
49
|
+
else # has_one
|
|
50
|
+
fk_val = related.public_send(of_reflection.foreign_key)
|
|
51
|
+
target_class.find_by(of_reflection.association_primary_key => fk_val)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The YAML value for a target: the find_by column on its `of:` relation.
|
|
56
|
+
# nil when the target has no related record (can't be represented).
|
|
57
|
+
def of_value_for(target)
|
|
58
|
+
related = target.public_send(@of)
|
|
59
|
+
related&.public_send(@find_by)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Declaration-time check: `of:` must name a 1:[0,1] association on the
|
|
63
|
+
# target. Raises ArgumentError at class load otherwise.
|
|
64
|
+
def validate_of_reflection!(node_label)
|
|
65
|
+
of_ref = target_class.reflect_on_association(@of)
|
|
66
|
+
unless of_ref
|
|
67
|
+
raise ArgumentError,
|
|
68
|
+
"`#{node_label}, of: #{@of.inspect}`: #{target_class} has no association `#{@of}`"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
return if singular_of_association?(of_ref)
|
|
72
|
+
|
|
73
|
+
raise ArgumentError,
|
|
74
|
+
"`#{node_label}, of: #{@of.inspect}`: `of:` must be a 1:[0,1] association " \
|
|
75
|
+
"(belongs_to or has_one); got #{of_ref.macro}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def singular_of_association?(reflection)
|
|
79
|
+
reflection.is_a?(ActiveRecord::Reflection::BelongsToReflection) ||
|
|
80
|
+
reflection.is_a?(ActiveRecord::Reflection::HasOneReflection)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -16,6 +16,8 @@ module YamlExporter
|
|
|
16
16
|
# Only 1:[0,1] `of:` associations are permitted (belongs_to or has_one).
|
|
17
17
|
# Phase: :pre_save (sets the FK before record.save!).
|
|
18
18
|
class OneReferenceOf
|
|
19
|
+
include OfResolution
|
|
20
|
+
|
|
19
21
|
attr_reader :name, :owner_class, :find_by, :of
|
|
20
22
|
|
|
21
23
|
def initialize(name:, owner_class:, find_by:, of:)
|
|
@@ -30,17 +32,7 @@ module YamlExporter
|
|
|
30
32
|
"`one #{name.inspect}`: #{owner_class} has no association `#{name}`"
|
|
31
33
|
end
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
unless of_ref
|
|
35
|
-
raise ArgumentError,
|
|
36
|
-
"`one #{name.inspect}, of: #{of.inspect}`: #{target_class} has no association `#{of}`"
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
unless singular_association?(of_ref)
|
|
40
|
-
raise ArgumentError,
|
|
41
|
-
"`one #{name.inspect}, of: #{of.inspect}`: `of:` must be a 1:[0,1] association " \
|
|
42
|
-
"(belongs_to or has_one); got #{of_ref.macro}"
|
|
43
|
-
end
|
|
35
|
+
validate_of_reflection!("one #{name.inspect}")
|
|
44
36
|
end
|
|
45
37
|
|
|
46
38
|
def phase
|
|
@@ -63,16 +55,16 @@ module YamlExporter
|
|
|
63
55
|
return
|
|
64
56
|
end
|
|
65
57
|
|
|
66
|
-
|
|
67
|
-
unless
|
|
58
|
+
related = of_class.find_by(@find_by => value)
|
|
59
|
+
unless related
|
|
68
60
|
raise ActiveRecord::RecordNotFound,
|
|
69
|
-
"no #{
|
|
61
|
+
"no #{of_class} with #{@find_by}=#{value.inspect}"
|
|
70
62
|
end
|
|
71
63
|
|
|
72
|
-
target = find_target_via(
|
|
64
|
+
target = find_target_via(related)
|
|
73
65
|
unless target
|
|
74
66
|
raise ActiveRecord::RecordNotFound,
|
|
75
|
-
"no #{target_class} linked to #{
|
|
67
|
+
"no #{target_class} linked to #{of_class} #{@find_by}=#{value.inspect} via `#{@of}`"
|
|
76
68
|
end
|
|
77
69
|
|
|
78
70
|
record.public_send("#{@name}=", target)
|
|
@@ -82,46 +74,11 @@ module YamlExporter
|
|
|
82
74
|
target = record.public_send(@name)
|
|
83
75
|
return [@name.to_s, nil] if target.nil?
|
|
84
76
|
|
|
85
|
-
|
|
86
|
-
return [@name.to_s, nil] if through.nil?
|
|
87
|
-
|
|
88
|
-
[@name.to_s, through.public_send(@find_by)]
|
|
77
|
+
[@name.to_s, of_value_for(target)]
|
|
89
78
|
end
|
|
90
79
|
|
|
91
80
|
def schema_fragment
|
|
92
|
-
{ @name => { type: TypeInference.schema_type_for(
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
private
|
|
96
|
-
|
|
97
|
-
def of_reflection
|
|
98
|
-
@of_reflection ||= target_class.reflect_on_association(@of)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def through_class
|
|
102
|
-
@through_class ||= of_reflection.klass
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# Navigate from a resolved `through` record back to the target.
|
|
106
|
-
#
|
|
107
|
-
# belongs_to (:user on CorporateUser): FK is on the target.
|
|
108
|
-
# target_class.find_by(user_id: through.id)
|
|
109
|
-
#
|
|
110
|
-
# has_one (:profile on CorporateUser): FK is on the through record.
|
|
111
|
-
# target_class.find_by(id: through.corporate_user_id)
|
|
112
|
-
def find_target_via(through)
|
|
113
|
-
if of_reflection.is_a?(ActiveRecord::Reflection::BelongsToReflection)
|
|
114
|
-
pk_val = through.public_send(of_reflection.association_primary_key)
|
|
115
|
-
target_class.find_by(of_reflection.foreign_key => pk_val)
|
|
116
|
-
else # has_one
|
|
117
|
-
fk_val = through.public_send(of_reflection.foreign_key)
|
|
118
|
-
target_class.find_by(of_reflection.association_primary_key => fk_val)
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def singular_association?(reflection)
|
|
123
|
-
reflection.is_a?(ActiveRecord::Reflection::BelongsToReflection) ||
|
|
124
|
-
reflection.is_a?(ActiveRecord::Reflection::HasOneReflection)
|
|
81
|
+
{ @name => { type: TypeInference.schema_type_for(of_class, @find_by) } }
|
|
125
82
|
end
|
|
126
83
|
end
|
|
127
84
|
end
|
data/lib/yaml_exporter.rb
CHANGED
|
@@ -63,6 +63,7 @@ require 'yaml'
|
|
|
63
63
|
require_relative 'yaml_exporter/structure'
|
|
64
64
|
require_relative 'yaml_exporter/type_inference'
|
|
65
65
|
require_relative 'yaml_exporter/nodes/attribute'
|
|
66
|
+
require_relative 'yaml_exporter/nodes/of_resolution'
|
|
66
67
|
require_relative 'yaml_exporter/nodes/one_owned'
|
|
67
68
|
require_relative 'yaml_exporter/nodes/one_reference'
|
|
68
69
|
require_relative 'yaml_exporter/nodes/one_reference_of'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: yaml_exporter
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Anatoly Zelenin
|
|
@@ -108,6 +108,7 @@ files:
|
|
|
108
108
|
- lib/yaml_exporter/nodes/many_positional.rb
|
|
109
109
|
- lib/yaml_exporter/nodes/many_reference.rb
|
|
110
110
|
- lib/yaml_exporter/nodes/many_through.rb
|
|
111
|
+
- lib/yaml_exporter/nodes/of_resolution.rb
|
|
111
112
|
- lib/yaml_exporter/nodes/one_owned.rb
|
|
112
113
|
- lib/yaml_exporter/nodes/one_reference.rb
|
|
113
114
|
- lib/yaml_exporter/nodes/one_reference_of.rb
|