cocina-models 0.29.0 → 0.30.0

Sign up to get free protection for your applications and to get access to all the features.
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