cocina-models 0.29.0 → 0.30.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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +10 -3
  3. data/README.md +11 -0
  4. data/cocina-models.gemspec +5 -0
  5. data/exe/generator +9 -0
  6. data/lib/cocina/generator.rb +18 -0
  7. data/lib/cocina/generator/generator.rb +80 -0
  8. data/lib/cocina/generator/schema.rb +84 -0
  9. data/lib/cocina/generator/schema_array.rb +20 -0
  10. data/lib/cocina/generator/schema_base.rb +49 -0
  11. data/lib/cocina/generator/schema_ref.rb +16 -0
  12. data/lib/cocina/generator/schema_value.rb +60 -0
  13. data/lib/cocina/generator/vocab.rb +63 -0
  14. data/lib/cocina/models.rb +51 -25
  15. data/lib/cocina/models/access.rb +10 -0
  16. data/lib/cocina/models/admin_policy.rb +13 -56
  17. data/lib/cocina/models/admin_policy_administrative.rb +11 -0
  18. data/lib/cocina/models/administrative.rb +14 -0
  19. data/lib/cocina/models/catalog_link.rb +4 -1
  20. data/lib/cocina/models/collection.rb +21 -31
  21. data/lib/cocina/models/collection_identification.rb +9 -0
  22. data/lib/cocina/models/description.rb +1 -8
  23. data/lib/cocina/models/dro.rb +34 -70
  24. data/lib/cocina/models/dro_access.rb +16 -0
  25. data/lib/cocina/models/dro_structural.rb +14 -0
  26. data/lib/cocina/models/embargo.rb +16 -0
  27. data/lib/cocina/models/file.rb +20 -36
  28. data/lib/cocina/models/file_administrative.rb +10 -0
  29. data/lib/cocina/models/file_set.rb +8 -15
  30. data/lib/cocina/models/file_set_structural.rb +9 -0
  31. data/lib/cocina/models/geographic.rb +10 -0
  32. data/lib/cocina/models/identification.rb +11 -0
  33. data/lib/cocina/models/message_digest.rb +17 -0
  34. data/lib/cocina/models/presentation.rb +12 -0
  35. data/lib/cocina/models/release_tag.rb +12 -7
  36. data/lib/cocina/models/request_admin_policy.rb +15 -3
  37. data/lib/cocina/models/request_collection.rb +21 -4
  38. data/lib/cocina/models/request_dro.rb +32 -11
  39. data/lib/cocina/models/request_dro_structural.rb +13 -0
  40. data/lib/cocina/models/request_file.rb +15 -6
  41. data/lib/cocina/models/request_file_set.rb +7 -9
  42. data/lib/cocina/models/request_file_set_structural.rb +9 -0
  43. data/lib/cocina/models/sequence.rb +2 -5
  44. data/lib/cocina/models/title.rb +12 -0
  45. data/lib/cocina/models/validator.rb +22 -0
  46. data/lib/cocina/models/version.rb +1 -1
  47. data/lib/cocina/models/vocab.rb +45 -60
  48. data/openapi.yml +695 -0
  49. metadata +101 -9
  50. data/lib/cocina/models/admin_policy_attributes.rb +0 -21
  51. data/lib/cocina/models/collection_attributes.rb +0 -22
  52. data/lib/cocina/models/dro_attributes.rb +0 -22
  53. data/lib/cocina/models/file_attributes.rb +0 -25
  54. data/lib/cocina/models/file_set_attributes.rb +0 -16
  55. data/lib/cocina/models/types.rb +0 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76b2604162a420ba454bf72d767dd17cffd588c585644e7af5713a626528c354
4
- data.tar.gz: a120a35df55448923beb33b256001ea711de2a41b89a6b353e7eb8770aea160c
3
+ metadata.gz: ae254699614b4355dea34dab3e0ef324123b21d30d592c24ceffbad5fed3515e
4
+ data.tar.gz: da8eecbff7bec360ed56445d61fc558b4a18684df9f0f92bc4a98668e2d16dac
5
5
  SHA512:
6
- metadata.gz: 8339f6d67fa9ce0d717383e377d68c935ec77b6eab99248a74ea0b70ef74379c449a9a004942d5495df97f3a00aedb671323cdd9a1f040961d7b85c285094c64
7
- data.tar.gz: c1e9c0dd4dbd1c079e863d914519e7c173d79eb85e164a1c2121b69b23d87b3323823c93e52fcd4227fde46338c6987bef4ec75d85b3517a62869ac5eb40a259
6
+ metadata.gz: 4c6c2e1beb7cd1b28d73bee2641be223497e18764bbffae9953fde54995336ae970be70e8d1ac31fa96f131ad9c17a02b2a5ef7b9e9beb098ecf9925899333ad
7
+ data.tar.gz: 53246fecec8a7e4516aa3fef318dddfa644158678f4894d1eeb1285d0c247b6a2bdafd68a491ae18b19e41fa5a195bc2f078ba303adb9f83f37b26c4121585cf
@@ -4,13 +4,16 @@ inherit_from: .rubocop_todo.yml
4
4
  require:
5
5
  - rubocop-rspec
6
6
 
7
+ Metrics/LineLength:
8
+ Max: 114
9
+ Exclude:
10
+ - lib/cocina/models/*
11
+
7
12
  Metrics/BlockLength:
8
13
  Exclude:
14
+ - cocina-models.gemspec
9
15
  - spec/cocina/**/*
10
16
 
11
- Metrics/LineLength:
12
- Max: 114
13
-
14
17
  Metrics/MethodLength:
15
18
  Max: 14
16
19
 
@@ -19,3 +22,7 @@ RSpec/MultipleExpectations:
19
22
 
20
23
  RSpec/ExampleLength:
21
24
  Max: 18
25
+
26
+ Style/Documentation:
27
+ Exclude:
28
+ - lib/cocina/models/*
data/README.md CHANGED
@@ -11,6 +11,17 @@ It provides a way for consumers to validate objects against models using dry-str
11
11
 
12
12
  This is a work in progress that will ultimately implement the full [COCINA data model](http://sul-dlss.github.io/cocina-models/). See also [architecture documentation](https://sul-dlss.github.io/taco-truck/COCINA.html#cocina-data-models--shapes).
13
13
 
14
+ ## Generate models from openapi.yml
15
+
16
+ ### All
17
+ ```
18
+ exe/generator generate
19
+ ```
20
+
21
+ ### Single model
22
+ ```
23
+ exe/generator generate_schema DRO
24
+ ```
14
25
 
15
26
  ## Generate Documentation
16
27
 
@@ -23,11 +23,16 @@ Gem::Specification.new do |spec|
23
23
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
24
  spec.require_paths = ['lib']
25
25
 
26
+ spec.add_dependency 'activesupport'
26
27
  spec.add_dependency 'dry-struct', '~> 1.0'
27
28
  spec.add_dependency 'dry-types', '~> 1.1'
29
+ spec.add_dependency 'openapi3_parser' # Parsing openapi doc
30
+ spec.add_dependency 'openapi_parser' # Validating openapi requests
31
+ spec.add_dependency 'thor'
28
32
  spec.add_dependency 'zeitwerk', '~> 2.1'
29
33
 
30
34
  spec.add_development_dependency 'bundler', '~> 2.0'
35
+ spec.add_development_dependency 'committee'
31
36
  spec.add_development_dependency 'rake', '~> 12.0'
32
37
  spec.add_development_dependency 'rspec', '~> 3.0'
33
38
  spec.add_development_dependency 'rubocop', '~> 0.74.0'
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ $LOAD_PATH.unshift 'lib'
6
+
7
+ require 'cocina/generator'
8
+
9
+ Cocina::Generator::Generator.start(ARGV)
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zeitwerk'
4
+ require 'yaml'
5
+ require 'active_support/core_ext/string'
6
+ require 'thor'
7
+ require 'openapi3_parser'
8
+ require 'byebug'
9
+
10
+ loader = Zeitwerk::Loader.new
11
+ loader.push_dir(File.absolute_path("#{__FILE__}/../.."))
12
+ loader.setup
13
+
14
+ module Cocina
15
+ # Module for generating Cocina models from openapi.
16
+ module Generator
17
+ end
18
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Generator
5
+ # Class for generating Cocina models from openapi.
6
+ class Generator < Thor
7
+ include Thor::Actions
8
+
9
+ class_option :openapi, desc: 'Path of openapi.yml', default: 'openapi.yml'
10
+ class_option :output, desc: 'Path for output', default: 'lib/cocina/models'
11
+
12
+ def self.source_root
13
+ File.dirname(__FILE__)
14
+ end
15
+
16
+ desc 'generate', 'generate for all schemas'
17
+ def generate
18
+ clean_output
19
+
20
+ schemas.keys.each do |schema_name|
21
+ schema = schema_for(schema_name)
22
+ generate_for(schema) if schema
23
+ end
24
+
25
+ generate_vocab
26
+ end
27
+
28
+ desc 'generate_schema SCHEMA_NAME', 'generate for SCHEMA_NAME'
29
+ def generate_schema(schema_name)
30
+ schema = schema_for(schema_name)
31
+ raise 'Cannot generate' if schema.nil?
32
+
33
+ FileUtils.mkdir_p(options[:output])
34
+
35
+ generate_for(schema)
36
+ end
37
+
38
+ desc 'generate_vocab', 'generate vocab'
39
+ def generate_vocab
40
+ vocab = Vocab.new(schemas)
41
+ filepath = "#{options[:output]}/#{vocab.filename}"
42
+ FileUtils.rm_f(filepath)
43
+
44
+ create_file filepath, vocab.generate
45
+ run("rubocop -a #{filepath}")
46
+ end
47
+
48
+ private
49
+
50
+ def schemas
51
+ @schemas ||= Openapi3Parser.load_file(options[:openapi]).components.schemas
52
+ end
53
+
54
+ def schema_for(schema_name)
55
+ schema_doc = schemas[schema_name]
56
+ return nil if schema_doc.nil? || schema_doc.type != 'object'
57
+
58
+ Schema.new(schema_doc)
59
+ end
60
+
61
+ def generate_for(schema)
62
+ filepath = "#{options[:output]}/#{schema.filename}"
63
+ FileUtils.rm_f(filepath)
64
+
65
+ create_file filepath, schema.generate
66
+ run("rubocop -a #{filepath}")
67
+ end
68
+
69
+ def clean_output
70
+ FileUtils.mkdir_p(options[:output])
71
+ files = Dir.glob("#{options[:output]}/*.rb")
72
+ # Leave alone
73
+ files.delete("#{options[:output]}/version.rb")
74
+ files.delete("#{options[:output]}/checkable.rb")
75
+ files.delete("#{options[:output]}/validator.rb")
76
+ FileUtils.rm_f(files)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Generator
5
+ # Class for generating from an openapi schema
6
+ class Schema < SchemaBase
7
+ def schema_properties
8
+ @schema_properties ||= schema_doc.properties.map do |key, properties_doc|
9
+ property_class_for(properties_doc).new(properties_doc,
10
+ key: key,
11
+ required: schema_doc.requires?(properties_doc),
12
+ parent: self)
13
+ end
14
+ end
15
+
16
+ def generate
17
+ <<~RUBY
18
+ # frozen_string_literal: true
19
+
20
+ module Cocina
21
+ module Models
22
+ class #{name} < Struct
23
+
24
+ #{types}
25
+
26
+ #{model_attributes}
27
+
28
+ #{validate}
29
+ end
30
+ end
31
+ end
32
+ RUBY
33
+ end
34
+
35
+ private
36
+
37
+ def property_class_for(properties_doc)
38
+ case properties_doc.type
39
+ when 'object'
40
+ # As a useful simplification, all objects must be references to schemas.
41
+ raise "Must use a reference for #{schema_doc.inspect}" unless properties_doc.name
42
+
43
+ SchemaRef
44
+ when 'array'
45
+ SchemaArray
46
+ else
47
+ SchemaValue
48
+ end
49
+ end
50
+
51
+ def model_attributes
52
+ schema_properties.map(&:generate).join("\n")
53
+ end
54
+
55
+ def types
56
+ type_properties_doc = schema_doc.properties['type']
57
+ return '' if type_properties_doc.nil?
58
+
59
+ types_list = type_properties_doc.enum.map { |item| "'#{item}'" }.join(",\n ")
60
+
61
+ <<~RUBY
62
+ include Checkable
63
+
64
+ TYPES = [#{types_list}].freeze
65
+ RUBY
66
+ end
67
+
68
+ def validate
69
+ return '' unless validatable?
70
+
71
+ <<~RUBY
72
+ def self.new(attributes = default_attributes, safe = false, validate = true, &block)
73
+ Validator.validate(self, attributes.with_indifferent_access) if validate
74
+ super(attributes, safe, &block)
75
+ end
76
+ RUBY
77
+ end
78
+
79
+ def validatable?
80
+ !schema_doc.node_context.document.paths["/validate/#{schema_doc.name}"].nil?
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Generator
5
+ # Class for generating from an openapi array
6
+ class SchemaArray < SchemaBase
7
+ def generate
8
+ "attribute :#{name.camelize(:lower)}, Types::Strict::Array.of(#{schema_doc.items.name})#{omittable}"
9
+ end
10
+
11
+ def omittable
12
+ if required
13
+ '.default([].freeze)'
14
+ else
15
+ '.meta(omittable: true)'
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Generator
5
+ # Base class for generating from openapi
6
+ class SchemaBase
7
+ attr_reader :schema_doc, :key, :required, :parent
8
+
9
+ def initialize(schema_doc, key: nil, required: false, parent: nil)
10
+ @schema_doc = schema_doc
11
+ @key = key
12
+ @required = required
13
+ @parent = parent
14
+ end
15
+
16
+ def filename
17
+ "#{name.underscore}.rb"
18
+ end
19
+
20
+ def name
21
+ key || schema_doc.name
22
+ end
23
+
24
+ def omittable
25
+ return '' if required
26
+
27
+ '.meta(omittable: true)'
28
+ end
29
+
30
+ def quote(item)
31
+ return item unless schema_doc.type == 'string'
32
+
33
+ "'#{item}'"
34
+ end
35
+
36
+ def description
37
+ return '' unless schema_doc.description
38
+
39
+ "# #{schema_doc.description}\n"
40
+ end
41
+
42
+ def example
43
+ return '' unless schema_doc.example
44
+
45
+ "# example: #{schema_doc.example}\n"
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Generator
5
+ # Class for generating from an openapi reference
6
+ class SchemaRef < SchemaBase
7
+ def generate
8
+ if required
9
+ "attribute(:#{name.camelize(:lower)}, #{schema_doc.name}.default { #{schema_doc.name}.new })"
10
+ else
11
+ "attribute :#{name.camelize(:lower)}, #{schema_doc.name}.optional.meta(omittable: true)"
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Generator
5
+ # Class for generating from an openapi value
6
+ class SchemaValue < SchemaBase
7
+ # rubocop:disable Metrics/LineLength
8
+ def generate
9
+ "#{description}#{example}attribute :#{name.camelize(:lower)}, Types::#{dry_datatype}#{default}#{enum}#{omittable}"
10
+ end
11
+ # rubocop:enable Metrics/LineLength
12
+
13
+ private
14
+
15
+ def dry_datatype
16
+ case schema_doc.type
17
+ when 'integer'
18
+ 'Strict::Integer'
19
+ when 'string'
20
+ string_dry_datatype
21
+ when 'boolean'
22
+ 'Strict::Bool'
23
+ else
24
+ raise "#{schema_doc.type} not supported"
25
+ end
26
+ end
27
+
28
+ def string_dry_datatype
29
+ case schema_doc.format
30
+ when 'date-time'
31
+ 'Params::DateTime'
32
+ else
33
+ 'Strict::String'
34
+ end
35
+ end
36
+
37
+ def enum
38
+ return '' unless schema_doc.enum
39
+
40
+ items = use_types? ? "*#{parent.name}::TYPES" : schema_doc.enum.map { |item| quote(item) }.join(', ')
41
+
42
+ ".enum(#{items})"
43
+ end
44
+
45
+ def use_types?
46
+ parent.is_a?(Schema) && key == 'type'
47
+ end
48
+
49
+ def default
50
+ # If type is boolean and default is false, erroneously getting a nil.
51
+ # Assuming that if required, then default is false.
52
+ default = schema_doc.default
53
+ default = false if default.nil? && schema_doc.type == 'boolean' && required
54
+ return '' if default.nil?
55
+
56
+ ".default(#{quote(default)})"
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Generator
5
+ # Class for generating a vocab
6
+ class Vocab
7
+ def initialize(schemas)
8
+ @schemas = schemas
9
+ end
10
+
11
+ def filename
12
+ 'vocab.rb'
13
+ end
14
+
15
+ def generate
16
+ <<~RUBY
17
+ # frozen_string_literal: true
18
+
19
+ module Cocina
20
+ module Models
21
+ # A digital repository object. See http://sul-dlss.github.io/cocina-models/maps/DRO.json
22
+ class Vocab
23
+
24
+ #{vocab_methods}
25
+
26
+ end
27
+ end
28
+ end
29
+ RUBY
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :schemas
35
+
36
+ # rubocop:disable Style/MultilineBlockChain
37
+ def vocabs
38
+ schemas.values.map do |schema|
39
+ type_property = schema.properties['type']
40
+ type_property.nil? ? [] : type_property.enum.to_a
41
+ end
42
+ .flatten
43
+ .uniq
44
+ .sort
45
+ .filter { |vocab| vocab.start_with?('http://cocina.sul.stanford.edu/models') }
46
+ end
47
+ # rubocop:enable Style/MultilineBlockChain
48
+
49
+ def vocab_methods
50
+ # Note special handling of 3d
51
+ vocabs.map do |vocab|
52
+ name = vocab[38, vocab.size - 45].gsub('-', '_').gsub('3d', 'three_dimensional')
53
+ <<~RUBY
54
+ def self.#{name}
55
+ "#{vocab}"
56
+ end
57
+
58
+ RUBY
59
+ end.join("\n")
60
+ end
61
+ end
62
+ end
63
+ end