yarrow 0.7.2 → 0.7.3

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.
@@ -0,0 +1,46 @@
1
+ module Yarrow
2
+ module Schema
3
+ # Value object (with comparison by value equality). This just chucks back a
4
+ # Ruby struct but wraps the constructor with method advice that handles
5
+ # type checking and conversion.
6
+ class Value
7
+ def self.new(*slots, **fields, &block)
8
+ factory(*slots, **fields, &block)
9
+ end
10
+
11
+ def self.factory(*slots, **fields, &block)
12
+ if slots.empty? && fields.empty?
13
+ raise ArgumentError.new("missing attribute definition")
14
+ end
15
+
16
+ slots_spec, fields_spec = if fields.any?
17
+ raise ArgumentError.new("cannot use slots when field map is supplied") if slots.any?
18
+ [fields.keys, fields]
19
+ else
20
+ [slots, Hash[slots.map { |s| [s, :any]}]]
21
+ end
22
+
23
+ validator = Dictionary.new(fields_spec)
24
+
25
+ struct = Struct.new(*slots_spec, keyword_init: true, &block)
26
+
27
+ struct.define_method :initialize do |*args, **kwargs|
28
+ attr_values = if args.any?
29
+ raise ArgumentError.new("cannot mix slots and kwargs") if kwargs.any?
30
+ Hash[slots.zip(args)]
31
+ else
32
+ kwargs
33
+ end
34
+
35
+ validator.check(attr_values)
36
+ # TODO: type coercion or mapping decision goes here
37
+ super(**attr_values)
38
+
39
+ freeze
40
+ end
41
+
42
+ struct
43
+ end
44
+ end
45
+ end
46
+ end
data/lib/yarrow/schema.rb CHANGED
@@ -1,132 +1,41 @@
1
+ require "delegate"
2
+
1
3
  module Yarrow
2
4
  module Schema
3
5
  module Type
4
- class Any
5
- end
6
- end
7
-
8
- ##
9
- # Checks values plugged into each slot and runs any required validations
10
- # (validations not yet implemented).
11
- #
12
- # Current design throws on error rather than returns a boolean result.
13
- class Validator
14
- # @param fields_spec [Hash] defines the slots in the schema to validate against
15
- def initialize(fields_spec)
16
- @spec = fields_spec
17
- end
18
-
19
- def check(fields)
20
- missing_fields = @spec.keys.difference(fields.keys)
21
-
22
- if missing_fields.any?
23
- missing_fields.each do |field|
24
- raise "wrong number of args" unless @spec[field].eql?(Type::Any)
25
- end
26
- end
27
-
28
- mismatching_fields = fields.keys.difference(@spec.keys)
29
-
30
- raise "key does not exist" if mismatching_fields.any?
31
-
32
- fields.each do |(field, value)|
33
- raise "wrong data type" unless value.is_a?(@spec[field]) || @spec[field].eql?(Type::Any)
34
- end
35
-
36
- true
37
- end
38
- end
39
-
40
- ##
41
- # Value object (with comparison by value equality). This just chucks back a
42
- # Ruby struct but wraps the constructor with method advice that handles
43
- # validation (and eventually type coercion if !yagni).
44
- class Value
45
- def self.new(*slots, **fields, &block)
46
- factory(*slots, **fields, &block)
47
- end
48
-
49
- def self.factory(*slots, **fields, &block)
50
- if slots.empty? && fields.empty?
51
- raise ArgumentError.new("missing attribute definition")
52
- end
53
-
54
- slots_spec, fields_spec = if fields.any?
55
- raise ArgumentError.new("cannot use slots when field map is supplied") if slots.any?
56
- [fields.keys, fields]
57
- else
58
- [slots, Hash[slots.map { |s| [s, Type::Any]}]]
59
- end
60
-
61
- validator = Validator.new(fields_spec)
62
-
63
- struct = Struct.new(*slots_spec, keyword_init: true, &block)
64
-
65
- struct.define_method :initialize do |*args, **kwargs|
66
- attr_values = if args.any?
67
- raise ArgumentError.new("cannot mix slots and kwargs") if kwargs.any?
68
- Hash[slots.zip(args)]
69
- else
70
- kwargs
6
+ class Raw
7
+ class << self
8
+ def [](primitive)
9
+ @primitive = primitive
71
10
  end
72
11
 
73
- validator.check(attr_values)
74
- # TODO: type coercion or mapping decision goes here
75
- super(**attr_values)
76
-
77
- freeze
78
- end
79
-
80
- struct
81
- end
82
- end
83
-
84
- ##
85
- # Entity with comparison by reference equality. Generates attribute helpers
86
- # for a declared set of props. Used to replace Hashie::Mash without dragging
87
- # in a whole new library.
88
- class Entity
89
- class << self
90
- def attribute(name, value_type)
91
- # define_method("map_#{name}".to_sym) do |input|
92
- # value_type.coerce(input)
93
- # end
94
- dictionary[name] = value_type
95
- attr_reader(name)
96
- end
97
-
98
- def dictionary
99
- @dictionary ||= Hash.new
100
- end
101
- end
102
-
103
- def dictionary
104
- self.class.dictionary
105
- end
106
-
107
- def initialize(config)
108
- dictionary.each_key do |name|
109
- raise "missing declared attribute #{name}" unless dictionary.key?(name)
110
- end
111
-
112
- config.each_pair do |key, value|
113
- raise "#{key} not a declared attribute" unless dictionary.key?(key)
114
-
115
- defined_type = dictionary[key]
116
-
117
- unless value.is_a?(defined_type)
118
- raise "#{key} accepts #{defined_type} but #{value.class} given"
12
+ def new(input)
13
+ input
119
14
  end
120
-
121
- instance_variable_set("@#{key}", value)
122
15
  end
123
16
  end
124
- end
125
17
 
126
- def to_h
127
- dictionary.keys.reduce({}) do |h, name|
128
- h[name] = instance_variable_get("@#{name}")
18
+ class Any
129
19
  end
20
+ # class Attribute
21
+ # class << self
22
+ # def accepts(attr_type)
23
+ # @accepts = attr_type
24
+ # end
25
+ # end
26
+ #
27
+ # attr_accessor :value
28
+ # alias_method :__getobj__, :value
29
+ #
30
+ # def initialize(value)
31
+ # raise "Invalid type" unless @accepts.is_a?(value.class)
32
+ # @value = value
33
+ # end
34
+ # end
35
+ #
36
+ # class Text < Attribute
37
+ # accepts String
38
+ # end
130
39
  end
131
40
  end
132
41
  end
@@ -3,15 +3,30 @@ require "strings-case"
3
3
 
4
4
  module Yarrow
5
5
  module Symbols
6
+ # @param [Array<String>, Array<Symbol>] parts
7
+ # @return [Object]
8
+ def self.to_module_const(parts)
9
+ Object.const_get(parts.map { |atom|
10
+ Strings::Case.pascalcase(atom.to_s)
11
+ }.join("::"))
12
+ end
13
+
6
14
  # Converts an atomic content identifier to a live class constant.
15
+ #
16
+ # @param [Symbol, String] atom
17
+ # @return [Object]
7
18
  def self.to_const(atom)
8
19
  Object.const_get(Strings::Case.pascalcase(atom.to_s).to_sym)
9
20
  end
10
21
 
22
+ # @param [Symbol, String] atom
23
+ # @return [Symbol]
11
24
  def self.to_singular(atom)
12
25
  Strings::Inflection.singularize(atom.to_s).to_sym
13
26
  end
14
27
 
28
+ # @param [Symbol, String] atom
29
+ # @return [Symbol]
15
30
  def self.to_plural(atom)
16
31
  Strings::Inflection.pluralize(atom.to_s).to_sym
17
32
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module Yarrow
3
3
  APP_NAME = 'Yarrow'
4
- VERSION = '0.7.2'
4
+ VERSION = '0.7.3'
5
5
  end
data/lib/yarrow.rb CHANGED
@@ -6,18 +6,25 @@ require 'yarrow/extensions'
6
6
  require 'yarrow/symbols'
7
7
  require 'yarrow/logging'
8
8
  require 'yarrow/schema'
9
+ require 'yarrow/schema/types'
10
+ require 'yarrow/schema/definitions'
11
+ require 'yarrow/schema/dictionary'
12
+ require 'yarrow/schema/entity'
13
+ require 'yarrow/schema/value'
9
14
  require 'yarrow/config'
10
15
  require 'yarrow/configuration'
11
16
  require 'yarrow/console_runner'
12
17
  require 'yarrow/tools/front_matter'
13
18
  require 'yarrow/tools/content_utils'
14
19
  require 'yarrow/content/graph'
15
- require 'yarrow/content/object_type'
16
- require 'yarrow/content/source_collector'
17
- require 'yarrow/content/collection_expander'
20
+ require 'yarrow/content/source'
18
21
  require 'yarrow/content/expansion'
22
+ require 'yarrow/content/expansion_strategy'
19
23
  require 'yarrow/content/tree_expansion'
20
24
  require 'yarrow/content/manifest'
25
+ require 'yarrow/content/resource'
26
+ require 'yarrow/content/model'
27
+ require 'yarrow/content/policy'
21
28
  require 'yarrow/output/mapper'
22
29
  require 'yarrow/output/generator'
23
30
  require 'yarrow/output/context'
data/yarrow.gemspec CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
20
20
  spec.add_runtime_dependency 'em-websocket', '~> 0.5.1'
21
21
  spec.add_runtime_dependency 'strings-inflection', '~> 0.1'
22
22
  spec.add_runtime_dependency 'strings-case', '~> 0.3'
23
- spec.add_development_dependency 'bundler'
23
+ spec.add_runtime_dependency 'kramdown', '~> 2.4.0'
24
24
  spec.add_development_dependency 'rake', '~> 13.0'
25
25
  spec.add_development_dependency 'rspec', '~> 3.11'
26
26
  spec.add_development_dependency 'coveralls', '~> 0.8.23'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yarrow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.2
4
+ version: 0.7.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark Rickerby
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-08-01 00:00:00.000000000 Z
11
+ date: 2022-10-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mementus
@@ -109,19 +109,19 @@ dependencies:
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0.3'
111
111
  - !ruby/object:Gem::Dependency
112
- name: bundler
112
+ name: kramdown
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
- - - ">="
115
+ - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: '0'
118
- type: :development
117
+ version: 2.4.0
118
+ type: :runtime
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
- - - ">="
122
+ - - "~>"
123
123
  - !ruby/object:Gem::Version
124
- version: '0'
124
+ version: 2.4.0
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: rake
127
127
  requirement: !ruby/object:Gem::Requirement
@@ -203,13 +203,14 @@ files:
203
203
  - lib/yarrow/config.rb
204
204
  - lib/yarrow/configuration.rb
205
205
  - lib/yarrow/console_runner.rb
206
- - lib/yarrow/content/collection_expander.rb
207
206
  - lib/yarrow/content/expansion.rb
207
+ - lib/yarrow/content/expansion_strategy.rb
208
208
  - lib/yarrow/content/graph.rb
209
209
  - lib/yarrow/content/manifest.rb
210
- - lib/yarrow/content/object_type.rb
210
+ - lib/yarrow/content/model.rb
211
+ - lib/yarrow/content/policy.rb
212
+ - lib/yarrow/content/resource.rb
211
213
  - lib/yarrow/content/source.rb
212
- - lib/yarrow/content/source_collector.rb
213
214
  - lib/yarrow/content/tree_expansion.rb
214
215
  - lib/yarrow/content_map.rb
215
216
  - lib/yarrow/defaults.yml
@@ -228,9 +229,14 @@ files:
228
229
  - lib/yarrow/process/step_processor.rb
229
230
  - lib/yarrow/process/workflow.rb
230
231
  - lib/yarrow/schema.rb
232
+ - lib/yarrow/schema/definitions.rb
233
+ - lib/yarrow/schema/dictionary.rb
234
+ - lib/yarrow/schema/entity.rb
235
+ - lib/yarrow/schema/types.rb
231
236
  - lib/yarrow/schema/validations/array.rb
232
237
  - lib/yarrow/schema/validations/object.rb
233
238
  - lib/yarrow/schema/validations/string.rb
239
+ - lib/yarrow/schema/value.rb
234
240
  - lib/yarrow/server.rb
235
241
  - lib/yarrow/server/livereload.rb
236
242
  - lib/yarrow/source/graph.rb
@@ -262,7 +268,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
262
268
  - !ruby/object:Gem::Version
263
269
  version: '0'
264
270
  requirements: []
265
- rubygems_version: 3.1.2
271
+ rubygems_version: 3.3.7
266
272
  signing_key:
267
273
  specification_version: 4
268
274
  summary: Documentation generator based on a fluent data model.
@@ -1,241 +0,0 @@
1
- module Yarrow
2
- module Content
3
- class CollectionExpander
4
- include Yarrow::Tools::FrontMatter
5
-
6
- # If a list of object types is not provided, a default `pages` type is
7
- # created.
8
- def initialize(object_types=nil)
9
- @object_types = object_types || [
10
- Yarrow::Content::ObjectType.from_name(:pages)
11
- ]
12
- end
13
-
14
- def expand(graph)
15
- @object_types.each do |object_type|
16
- expand_nested(graph, object_type)
17
- end
18
- end
19
-
20
- def expand_nested(graph, content_type)
21
- strategy = TreeExpansion.new(graph)
22
- strategy.expand(content_type)
23
- end
24
-
25
- def expand_nested_legacy(graph, content_type)
26
- type = content_type.collection
27
- exts = content_type.extensions
28
-
29
- # If match path represents entire content dir, then include the entire
30
- # content dir instead of scanning from a subfolder matching the name of
31
- # the collection.
32
- start_node = if content_type.match_path == "."
33
- graph.n(:root)
34
- else
35
- graph.n(:root).out(name: type.to_s)
36
- end
37
-
38
- # Extract metadata from given start node
39
- data = extract_metadata(start_node, type)
40
-
41
- # Collects all nested collections in the subgraph for this content type
42
- subcollections = {}
43
- index = nil
44
-
45
- # Define alias for accessing metadata in the loop
46
- metadata = data
47
-
48
- # Scan and collect all nested directories under the top level source
49
- start_node.depth_first.each do |node|
50
- if node.label == :directory
51
- # Check if this entry has metadata defined at the top level
52
- if data[:collections]
53
- item = data[:collections].find { |c| c[:slug] == node.props[:slug] }
54
- metadata = item if item
55
- end
56
-
57
- # Create a collection node representing a collection of documents
58
- index = graph.create_node do |collection_node|
59
- collection_node.label = :collection
60
- collection_node.props[:type] = type
61
- collection_node.props[:name] = node.props[:name]
62
- collection_node.props[:slug] = node.props[:slug]
63
- collection_node.props[:title] = metadata[:title]
64
-
65
- # Override default status so that mapped index collections always show
66
- # up in the resulting document manifest, when they don’t have
67
- # associated metadata. This is the opposite of how individual pieces
68
- # of content behave (default to draft status if one isn’t supplied).
69
- collection_node.props[:status] = if data[:status]
70
- data[:status]
71
- else
72
- "published"
73
- end
74
-
75
- # TODO: URL generation might need to happen elsewhere
76
- collection_node.props[:url] = if data[:url]
77
- data[:url]
78
- else
79
- "#{node.props[:path].split('./content').last}/"
80
- end
81
- end
82
-
83
- # Add this collection id to the lookup table for edge construction
84
- subcollections[node.props[:path]] = index
85
-
86
- # Join the collection to its parent
87
- unless node.props[:slug] == type.to_s || !subcollections.key?(node.props[:entry].parent.to_s)
88
- graph.create_edge do |edge|
89
- edge.label = :child
90
- edge.from = subcollections[node.props[:entry].parent.to_s].id
91
- edge.to = index.id
92
- end
93
- end
94
- end
95
- end
96
-
97
- # If there are no subcollections then we need to look at the start node
98
- # TODO: test to verify if this could be used in all cases, not just
99
- # the situation where there are subfolders to be mapped.
100
- if subcollections.empty?
101
- # Collect files that match the content type extension and group them
102
- # under a common key for each slug (this is so we can merge multiple
103
- # files with the same name together into a single content type, a
104
- # specific pattern found in some legacy content folders).
105
- #
106
- # Ideally, this code should be deleted once we have a clean workflow
107
- # and can experiment with decoupling different strategies for
108
- # expansion/enrichment of content objects.
109
- objects = start_node.out(:file).all.select do |file|
110
- file.props[:name].end_with?(*exts)
111
- end.group_by do |file|
112
- file.props[:slug]
113
- end
114
-
115
- # This is a massive hack to deal with situations where we don’t
116
- # recurse down the list of directories. The best way to clean it up
117
- # will be to document the different supported mapping formats and
118
- # URL generation strategies and break these up into separate
119
- # traversal objects for each particular style of content organisation.
120
- if index.nil?
121
- index = graph.create_node do |collection_node|
122
- collection_node.label = :collection
123
- collection_node.props[:type] = type
124
- collection_node.props[:name] = type
125
- collection_node.props[:slug] = type.to_s
126
- collection_node.props[:title] = metadata[:title]
127
-
128
- # Override default status so that mapped index collections always show
129
- # up in the resulting document manifest, when they don’t have
130
- # associated metadata. This is the opposite of how individual pieces
131
- # of content behave (default to draft status if one isn’t supplied).
132
- collection_node.props[:status] = if data[:status]
133
- data[:status]
134
- else
135
- "published"
136
- end
137
-
138
- # TODO: URL generation might need to happen elsewhere
139
- collection_node.props[:url] = if data[:url]
140
- data[:url]
141
- else
142
- "/#{type}/"
143
- end
144
- end
145
- end
146
-
147
- build_content_nodes(graph, objects, type, index)
148
- end
149
-
150
- # Go through each subcollection and expand content nodes step by step.
151
- subcollections.each do |path, index|
152
- # Group files matching the same slug under a common key
153
- objects = graph.n(path: path).out(:file).all.select do |file|
154
- file.props[:name].end_with?(*exts)
155
- end.group_by do |file|
156
- file.props[:slug]
157
- end
158
-
159
- build_content_nodes(graph, objects, type, index)
160
- end
161
- end
162
-
163
- def build_content_nodes(graph, objects, type, parent_index)
164
- # TODO: this may need to use a strategy that can be overriden
165
- content_type = Yarrow::Symbols.to_singular(type)
166
-
167
- # Process collected content objects and generate entity nodes
168
- objects.each do |name, sources|
169
- item_node = graph.create_node do |node|
170
- # TODO: Rename this to :entry and support similar fields to Atom
171
- node.label = :item
172
- node.props[:name] = name
173
- node.props[:type] = content_type
174
-
175
- meta = {}
176
- content = ""
177
-
178
- sources.each do |source|
179
- body, data = process_content(source.props[:entry])
180
- meta.merge!(data) unless data.nil?
181
- content << body unless body.nil?
182
- end
183
-
184
- if meta[:url]
185
- # If a URL is explicitly proided in metadata then use it
186
- node.props[:url] = meta[:url]
187
- elsif meta[:permalink]
188
- # Support for legacy permalink attribute
189
- node.props[:url] = meta[:permalink]
190
- else
191
- # Default URL generation strategy when no explicit URL is provided
192
- # TODO: collection nodes need URL generation too
193
- # TODO: replace this with URL generation strategy
194
- # TODO: slug vs name - why do some nodes have 2 and some 3 props?
195
- node.props[:url] = if parent_index.props[:name].to_sym == parent_index.props[:type]
196
- "/#{parent_index.props[:type]}/#{name}"
197
- else
198
- "/#{parent_index.props[:type]}/#{parent_index.props[:slug]}/#{name}"
199
- end
200
- end
201
-
202
- # For now, we are storing title, url, etc on the top-level item.
203
- node.props[:title] = meta[:title]
204
-
205
- # TODO: What belongs on the entity and what belongs on the item?
206
- entity_props = meta.merge(body: content, name: meta[:id], url: node.props[:url])
207
-
208
-
209
- # TODO: consider whether to provide `body` on the item/document or at
210
- # the custom content type level.
211
- begin
212
- content_struct = Yarrow::Symbols.to_const(content_type)
213
- rescue
214
- # No immutable struct found: fall back to slower dynamically typed open struct
215
- require "ostruct"
216
- content_struct = OpenStruct
217
- end
218
-
219
- node.props[:entity] = content_struct.new(entity_props)
220
- end
221
-
222
- # Connect entity with source content
223
- sources.each do |source|
224
- graph.create_edge do |edge|
225
- edge.label = :source
226
- edge.from = item_node
227
- edge.to = source.id
228
- end
229
- end
230
-
231
- # Connect entity with parent collection
232
- graph.create_edge do |edge|
233
- edge.label = :child
234
- edge.from = parent_index
235
- edge.to = item_node
236
- end
237
- end
238
- end
239
- end
240
- end
241
- end
@@ -1,42 +0,0 @@
1
- gem "strings-inflection"
2
-
3
- module Yarrow
4
- module Content
5
- class ObjectType
6
- Value = Yarrow::Schema::Value.new(:collection, :entity, :extensions)
7
-
8
- DEFAULT_EXTENSIONS = [".md", ".yml", ".htm"]
9
-
10
- def self.from_name(name)
11
- new(Value.new(collection: name.to_sym))
12
- end
13
-
14
- def initialize(properties)
15
- unless properties.respond_to?(:collection) || properties.respond_to?(:entity)
16
- raise "Must provide a collection name or entity name"
17
- end
18
-
19
- @properties = properties
20
- end
21
-
22
- def collection
23
- return @properties.collection if @properties.collection
24
- Yarrow::Symbols.to_plural(@properties.entity)
25
- end
26
-
27
- def entity
28
- return @properties.entity if @properties.entity
29
- Yarrow::Symbols.to_singular(@properties.collection)
30
- end
31
-
32
- def extensions
33
- return @properties.extensions if @properties.extensions
34
- DEFAULT_EXTENSIONS
35
- end
36
-
37
- def match_path
38
- "."
39
- end
40
- end
41
- end
42
- end