yaks 0.7.6 → 0.7.7

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -0
  3. data/Rakefile +58 -38
  4. data/lib/yaks.rb +3 -4
  5. data/lib/yaks/attributes.rb +23 -10
  6. data/lib/yaks/config.rb +4 -0
  7. data/lib/yaks/configurable.rb +16 -21
  8. data/lib/yaks/format.rb +1 -1
  9. data/lib/yaks/format/collection_json.rb +9 -0
  10. data/lib/yaks/format/halo.rb +16 -13
  11. data/lib/yaks/html5_forms.rb +22 -1
  12. data/lib/yaks/mapper.rb +5 -5
  13. data/lib/yaks/mapper/attribute.rb +1 -1
  14. data/lib/yaks/mapper/class_methods.rb +3 -1
  15. data/lib/yaks/mapper/config.rb +9 -16
  16. data/lib/yaks/mapper/form.rb +41 -0
  17. data/lib/yaks/mapper/form/field.rb +62 -0
  18. data/lib/yaks/null_resource.rb +1 -1
  19. data/lib/yaks/resource.rb +7 -3
  20. data/lib/yaks/resource/form.rb +31 -0
  21. data/lib/yaks/runner.rb +11 -11
  22. data/lib/yaks/stateful_builder.rb +11 -7
  23. data/lib/yaks/util.rb +3 -2
  24. data/lib/yaks/version.rb +1 -1
  25. data/spec/acceptance/models.rb +1 -1
  26. data/spec/json/confucius.halo.json +2 -4
  27. data/spec/json/plant_collection.collection.json +4 -0
  28. data/spec/unit/yaks/attributes_spec.rb +79 -0
  29. data/spec/unit/yaks/configurable_spec.rb +32 -2
  30. data/spec/unit/yaks/format/collection_json_spec.rb +59 -27
  31. data/spec/unit/yaks/format/halo_spec.rb +3 -0
  32. data/spec/unit/yaks/mapper/{control → form}/field_spec.rb +3 -3
  33. data/spec/unit/yaks/mapper/{control_spec.rb → form_spec.rb} +12 -12
  34. data/spec/unit/yaks/mapper_spec.rb +10 -10
  35. data/spec/unit/yaks/null_resource_spec.rb +50 -7
  36. data/spec/unit/yaks/resource_spec.rb +4 -4
  37. data/spec/unit/yaks/runner_spec.rb +38 -2
  38. data/spec/unit/yaks/util_spec.rb +42 -0
  39. metadata +20 -58
  40. data/lib/yaks/mapper/control.rb +0 -82
  41. data/lib/yaks/resource/control.rb +0 -11
@@ -3,6 +3,7 @@
3
3
  module Yaks
4
4
  class Mapper
5
5
  module ClassMethods
6
+ extend Util::Deprecated
6
7
  include Forwardable,
7
8
  Util,
8
9
  FP
@@ -27,7 +28,7 @@ module Yaks
27
28
  :profile,
28
29
  :has_one,
29
30
  :has_many,
30
- :control
31
+ :form
31
32
  ]
32
33
 
33
34
  CONFIG_METHODS.each do |method_name|
@@ -40,6 +41,7 @@ module Yaks
40
41
  end
41
42
  end
42
43
 
44
+ deprecated_alias :control, :form
43
45
  end
44
46
  end
45
47
  end
@@ -1,10 +1,11 @@
1
1
  module Yaks
2
2
  class Mapper
3
3
  class Config
4
+ extend Configurable
5
+
4
6
  include Attributes.new(
5
- type: nil, attributes: [], links: [], associations: [], controls: []
6
- ),
7
- Configurable
7
+ type: nil, attributes: [], links: [], associations: [], forms: []
8
+ )
8
9
 
9
10
  def type(type = Undefined)
10
11
  return @type if type.equal?(Undefined)
@@ -16,19 +17,11 @@ module Yaks
16
17
  append_to(:attributes, *attrs.map(&Attribute.method(:new)))
17
18
  end
18
19
 
19
- config_method :link, create: Link, append_to: :links
20
- config_method :has_one, create: HasOne, append_to: :associations
21
- config_method :has_many, create: HasMany, append_to: :associations
22
- config_method :attribute, create: Attribute, append_to: :attributes
23
- config_method :attribute, create: Attribute, append_to: :attributes
24
- config_method :control,
25
- append_to: :controls,
26
- create: StatefulBuilder.new(
27
- Control,
28
- Control.anima.attribute_names +
29
- HTML5Forms::INPUT_TYPES +
30
- [:field]
31
- )
20
+ config_method :link, create: Link, append_to: :links
21
+ config_method :has_one, create: HasOne, append_to: :associations
22
+ config_method :has_many, create: HasMany, append_to: :associations
23
+ config_method :attribute, create: Attribute, append_to: :attributes
24
+ config_method :form, create: Form, append_to: :forms
32
25
  end
33
26
  end
34
27
  end
@@ -0,0 +1,41 @@
1
+ module Yaks
2
+ class Mapper
3
+ class Form
4
+ extend Util::Deprecated, Configurable
5
+ include Attributes.new(
6
+ name: nil, action: nil, title: nil, method: nil, media_type: nil, fields: []
7
+ )
8
+
9
+ deprecated_alias :href, :action
10
+
11
+ Builder = StatefulBuilder.new(
12
+ self,
13
+ self.attributes.names + HTML5Forms::INPUT_TYPES + [:field]
14
+ )
15
+
16
+ def self.create(name = nil, options = {}, &block)
17
+ Builder.build(new({name: name}.merge(options)), &block)
18
+ end
19
+
20
+ def add_to_resource(resource, mapper, _context)
21
+ resource.add_form(to_resource(mapper))
22
+ end
23
+
24
+ def to_resource(mapper)
25
+ attrs = {
26
+ fields: resource_fields(mapper),
27
+ action: mapper.expand_uri(action, true)
28
+ }
29
+ [:name, :title, :method, :media_type].each do |attr|
30
+ attrs[attr] = mapper.expand_value(public_send(attr))
31
+ end
32
+ Resource::Form.new(attrs)
33
+ end
34
+
35
+ def resource_fields(mapper)
36
+ fields.map { |field| field.to_resource(mapper) }
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,62 @@
1
+ module Yaks
2
+ class Mapper
3
+ class Form
4
+ class Field
5
+ extend Configurable
6
+ include Attributes.new(
7
+ :name,
8
+ label: nil,
9
+ options: []
10
+ ).add(HTML5Forms::FIELD_OPTIONS)
11
+
12
+ Builder = StatefulBuilder.new(self, attributes.names)
13
+
14
+ def self.create(*args)
15
+ attrs = args.last.instance_of?(Hash) ? args.pop : {}
16
+ if name = args.shift
17
+ attrs = attrs.merge(name: name)
18
+ end
19
+ new(attrs)
20
+ end
21
+
22
+ # Convert to a Resource::Form::Field, expanding any dynamic
23
+ # values
24
+ def to_resource(mapper)
25
+ Resource::Form::Field.new(
26
+ resource_attributes.each_with_object({}) do |attr, attrs|
27
+ attrs[attr] = mapper.expand_value(public_send(attr))
28
+ end.merge(options: options.map(&:to_resource))
29
+ )
30
+ end
31
+
32
+ # All attributes that can be converted 1-to-1 to
33
+ # Resource::Form::Field
34
+ def resource_attributes
35
+ self.class.attributes.names - [:options]
36
+ end
37
+
38
+ # <option>, as used in a <select>
39
+ class Option
40
+ include Attributes.new(:value, :label, selected: false)
41
+
42
+ def self.create(value, opts = {})
43
+ new(opts.merge(value: value))
44
+ end
45
+
46
+ def to_resource
47
+ to_h #placeholder
48
+ end
49
+ end
50
+
51
+ config_method :option, create: Option, append_to: :options
52
+ end #Field
53
+
54
+ config_method :field, create: Field::Builder, append_to: :fields
55
+
56
+ HTML5Forms::INPUT_TYPES.each do |type|
57
+ config_method type, create: Field::Builder, append_to: :fields, defaults: { type: type }
58
+ end
59
+
60
+ end # Form
61
+ end # Mapper
62
+ end # Yaks
@@ -39,7 +39,7 @@ module Yaks
39
39
  raise UnsupportedOperationError, "Operation #{__method__} not supported on #{self.class}"
40
40
  end
41
41
 
42
- def add_control(_control)
42
+ def add_form(_form)
43
43
  raise UnsupportedOperationError, "Operation #{__method__} not supported on #{self.class}"
44
44
  end
45
45
 
data/lib/yaks/resource.rb CHANGED
@@ -6,7 +6,7 @@ module Yaks
6
6
  links: [],
7
7
  attributes: {},
8
8
  subresources: [],
9
- controls: []
9
+ forms: []
10
10
  )
11
11
 
12
12
  def initialize(attrs = {})
@@ -18,6 +18,10 @@ module Yaks
18
18
  attributes[attr]
19
19
  end
20
20
 
21
+ def find_form(name)
22
+ forms.find { |form| form.name == name }
23
+ end
24
+
21
25
  def seq
22
26
  [self]
23
27
  end
@@ -56,8 +60,8 @@ module Yaks
56
60
  append_to(:links, link)
57
61
  end
58
62
 
59
- def add_control(control)
60
- append_to(:controls, control)
63
+ def add_form(form)
64
+ append_to(:forms, form)
61
65
  end
62
66
 
63
67
  def add_subresource(subresource)
@@ -0,0 +1,31 @@
1
+ module Yaks
2
+ class Resource
3
+ class Form
4
+ include Yaks::Mapper::Form.attributes
5
+
6
+ def [](name)
7
+ fields.find {|field| field.name == name}.value
8
+ end
9
+
10
+ def values
11
+ fields.each_with_object({}) do |field, values|
12
+ values[field.name] = field.value
13
+ end
14
+ end
15
+
16
+ class Field
17
+ include Yaks::Mapper::Form::Field.attributes.add(:error => nil)
18
+
19
+ def value(arg = Undefined)
20
+ return @value if arg.eql?(Undefined)
21
+ if type == :select
22
+ selected = options.find { |option| option.selected }
23
+ selected.value if selected
24
+ else
25
+ update(value: arg)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
data/lib/yaks/runner.rb CHANGED
@@ -8,9 +8,16 @@ module Yaks
8
8
  def_delegators :config, :policy, :default_format, :format_options, :primitivize, :serializers
9
9
 
10
10
  def call
11
- steps.inject(object) {|memo, (_, step)| step.call(memo, env) }
11
+ process(steps, object)
12
+ end
13
+
14
+ def map(object)
15
+ process(insert_hooks([[:map, mapper]]), object)
16
+ end
17
+
18
+ def process(operations, input)
19
+ operations.inject(input) {|memo, (_, step)| step.call(memo, env) }
12
20
  end
13
- alias result call
14
21
 
15
22
  def context
16
23
  {
@@ -38,7 +45,7 @@ module Yaks
38
45
  format_class.media_type
39
46
  end
40
47
 
41
- def format
48
+ def format_name
42
49
  format_class.format_name
43
50
  end
44
51
 
@@ -74,13 +81,6 @@ module Yaks
74
81
  end
75
82
  memoize :primitivizer
76
83
 
77
- # @param [Hash] opts
78
- # @return [String]
79
- def format_name
80
- options.fetch(:format) { default_format }
81
- end
82
- memoize :format_name
83
-
84
84
  def serializer
85
85
  serializers.fetch(format_class.serializer)
86
86
  end
@@ -93,7 +93,7 @@ module Yaks
93
93
  def insert_hooks(steps)
94
94
  hooks.inject(steps) do |steps, (type, target_step, name, hook)|
95
95
  steps.flat_map do |step_name, callable|
96
- if step_name.eql? target_step
96
+ if step_name.equal? target_step
97
97
  case type
98
98
  when :before
99
99
  [[name, hook], [step_name, callable]]
@@ -6,27 +6,31 @@ module Yaks
6
6
  # @example
7
7
  #
8
8
  # # This code
9
- # Control.create(:search)
9
+ # Form.create(:search)
10
10
  # .method("POST")
11
11
  # .action("/search")
12
12
  #
13
13
  # # Can be written as
14
- # StatefulBuilder.new(Control, [:method, :action]).create(:search) do
14
+ # StatefulBuilder.new(Form, [:method, :action]).create(:search) do
15
15
  # method "POST"
16
16
  # action "/search"
17
17
  # end
18
18
  #
19
19
  class StatefulBuilder < BasicObject
20
20
  def create(*args, &block)
21
- @state = @klass.create(*args)
21
+ build(@klass.create(*args), &block)
22
+ end
23
+
24
+ def build(init_state, &block)
25
+ @state = init_state
22
26
  instance_eval(&block) if block
23
27
  @state
24
28
  end
25
29
 
26
- def initialize(klass, methods)
30
+ def initialize(klass, methods = nil)
27
31
  @klass = klass
28
- @methods = methods
29
- StatefulMethods.new(methods).send(:extend_object, self)
32
+ @methods = methods || klass.attributes.names
33
+ StatefulMethods.new(@methods).send(:extend_object, self)
30
34
  end
31
35
 
32
36
  def validate_state(method_name, args)
@@ -40,7 +44,7 @@ module Yaks
40
44
  end
41
45
 
42
46
  def inspect
43
- "#<#{self.class} #{@klass} #{@methods.inspect}>"
47
+ "#<StatefulBuilder #{@klass} #{@methods.inspect}>"
44
48
  end
45
49
 
46
50
  class StatefulMethods < ::Module
data/lib/yaks/util.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
+
2
3
  module Yaks
3
4
  module Util
4
5
  extend self
@@ -45,7 +46,7 @@ module Yaks
45
46
  context.instance_exec(&maybe_proc)
46
47
  end
47
48
  else
48
- maybe_proc.to_proc.()
49
+ maybe_proc.to_proc.call()
49
50
  end
50
51
  else
51
52
  maybe_proc
@@ -56,7 +57,7 @@ module Yaks
56
57
  def deprecated_alias(name, actual)
57
58
  define_method name do |*args, &block|
58
59
  $stderr.puts "WARNING: #{self.class}##{name} is deprecated, use `#{actual}'. at #{caller.first}"
59
- send(actual, *args, &block)
60
+ public_send(actual, *args, &block)
60
61
  end
61
62
  end
62
63
  end
data/lib/yaks/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Yaks
2
- VERSION = '0.7.6'
2
+ VERSION = '0.7.7'
3
3
  end
@@ -28,7 +28,7 @@ class ScholarMapper < LiteratureBaseMapper
28
28
  link 'http://literature.example.com/rels/quotes', 'http://literature.example.com/quotes/?author={downcased_pinyin}&q={query}', expand: [:downcased_pinyin], title: 'Search for quotes'
29
29
  link :self, 'http://literature.example.com/authors/{downcased_pinyin}'
30
30
 
31
- control :search do
31
+ form :search do
32
32
  title 'Find a Scholar'
33
33
  method 'POST'
34
34
  media_type 'application/x-www-form-urlencoded'
@@ -65,14 +65,12 @@
65
65
  {
66
66
  "name": "name",
67
67
  "label": "Scholar Name",
68
- "type": "text",
69
- "value": null
68
+ "type": "text"
70
69
  },
71
70
  {
72
71
  "name": "pinyin",
73
72
  "label": "Hanyu Pinyin",
74
- "type": "text",
75
- "value": null
73
+ "type": "text"
76
74
  }
77
75
  ]
78
76
  }
@@ -27,6 +27,10 @@
27
27
  { "rel": "profile", "href": "http://api.example.com/doc/plant" }
28
28
  ]
29
29
  }
30
+ ],
31
+ "links": [
32
+ { "href": "http://api.example.com/plants", "rel": "self" },
33
+ { "href": "http://api.example.com/doc/plant_collection", "rel": "profile" }
30
34
  ]
31
35
  }
32
36
  }
@@ -63,3 +63,82 @@ RSpec.describe Yaks::Attributes do
63
63
  end
64
64
  end
65
65
  end
66
+
67
+ RSpec.describe Yaks::Attributes::InstanceMethods do
68
+ let(:widget) do
69
+ Class.new do
70
+ include Yaks::Attributes.new(:color, :size, options: {})
71
+ def self.name ; 'Widget' ; end
72
+ end
73
+ end
74
+
75
+ let(:widget_container) do
76
+ Class.new do
77
+ include Yaks::Attributes.new(widgets: [])
78
+ def self.name ; 'WidgetContainer' ; end
79
+ end
80
+ end
81
+
82
+ let(:fixed_width) do
83
+ Class.new do
84
+ def initialize(width)
85
+ @width = width
86
+ end
87
+
88
+ def inspect
89
+ "#" * @width
90
+ end
91
+ end
92
+ end
93
+
94
+ describe '#pp' do
95
+ it 'should render correctly' do
96
+ expect(widget_container.new(widgets: [
97
+ widget.new(color: :green, size: 7),
98
+ widget.new(color: :blue, size: 9, options: {foo: :bar})
99
+ ]).pp).to eql "
100
+ WidgetContainer.new(
101
+ widgets: [
102
+ Widget.new(color: :green, size: 7),
103
+ Widget.new(color: :blue, size: 9, options: {:foo=>:bar})
104
+ ]
105
+ )
106
+ ".strip
107
+ end
108
+
109
+ it 'should inline short arrays' do
110
+ expect(widget_container.new(widgets: [
111
+ fixed_width.new(23),
112
+ fixed_width.new(22)
113
+ ]).pp).to eql "WidgetContainer.new(widgets: [#######################, ######################])"
114
+ end
115
+
116
+ it 'should put longer arrays on multiple lines' do
117
+ expect(widget_container.new(widgets: [
118
+ fixed_width.new(23),
119
+ fixed_width.new(23)
120
+ ]).pp).to eql "WidgetContainer.new(\n widgets: [\n #######################,\n #######################\n ]\n)"
121
+ end
122
+
123
+ it 'should puts attributes on multiple lines if total length exceeds 50 chars' do
124
+ expect(widget.new(color: fixed_width.new(18), size: fixed_width.new(18)).pp).to match /\n/
125
+ expect(widget.new(color: fixed_width.new(18), size: fixed_width.new(17)).pp).to_not match /\n/
126
+ end
127
+ end
128
+
129
+ describe '#append_to' do
130
+ it 'should append to a named collection' do
131
+ expect(widget_container.new(widgets: [:bar]).append_to(:widgets, :foo)).to eql widget_container.new(widgets: [:bar, :foo])
132
+ end
133
+ end
134
+
135
+ describe '#initialize' do
136
+ it 'should take hash-based args' do
137
+ expect(widget_container.new(widgets: [:bar])).to eql widget_container.new.widgets([:bar])
138
+ end
139
+
140
+ it 'should use defaults when available' do
141
+ expect(widget.new(color: :blue, size: 3).options).to eql({})
142
+ end
143
+ end
144
+ end